Hacker News new | ask | show | jobs
by escherize 3648 days ago
I'd like to add one: let your tools do the work for you. It may seem like a pain to learn the tooling behind what you do, but once you internalize it, it becomes a superpower.

An example is that I use Clojure Refactor Mode (with CIDER) for emacs. A trick (and treat) that a lot of Clojure code uses is the arrow macros: -> and ->>. Clojure Refactor Mode has thread-first, thread-first-all, thread-last, thread-last-all and unwind. Since I've committed those to my long term memory, I can just call thread-first-all on something like:

    (reduce * (repeat 4 (count (str (* 100 2)))))
and get:

    (->> 2
         (* 100)
         str
         count
         (repeat 4)
         (reduce *))
This is so huge, because many times changing the levels of threading makes reasoning about the code so much easier.
3 comments

I agree with the tooling part.

I tend to prefer the functional version to threading because (1) honestly, like fluent interfaces, it seems overused (2) function application is damn easy to read and (3) as soon as you have as many nested operations, it can and should be refactored into meaningful auxiliary functions. The first line reads as:

Multiply all ... 4 copies of ... the length of ... the string "200". So, basically, the length of that string multiplied by itself 4 times? (exponent).

The other form is more like step-by-step instructions, which is nice. However results are implicitly being passed at the first or last argument (I don't always remember which), and most everyday functions don't fit in the first/last category.

One of the nice things about the Clojure standard library is that most everyday functions actually do fit the first/last category (by design). Functions operating on sequences (map, filter, reduce, etc) take the sequence as the last argument and are suited for use with the ->> macro, while functions operating on data structures in a non-sequence context typically take the data structure first (assoc, conj, update) and are good for ->. So you get either:

    (->> (range 10)
         (map inc)
         (filter even?)
         (take 2)) ;=> '(2 4)
or

    (-> {:body {:some {:json :data}}}
        (assoc-in [:body :some :more-json] :more-data)
        (assoc :status 200)
        (update :body json/generate-string))
    ;;=> {:body "{\"some\":{\"json\":\"data\",\"more-json\":\"more-data\"}}", :status 200}
        
It doesn't work all the time, obviously, and it can be easy to get carried away with 15 threaded map/filter/reduce calls that should be factored into separate functions, but most of the time I find it to be a nice idiom that substantially improves readability.
That's like the function composition operator [1] in Haskell, right? Very neat :D I wonder if there's an equivalent macro in Scala ...

[1]: http://lambda.jstolarek.com/2012/03/function-composition-and...

It's more like the pipe operator in ocaml (http://blog.shaynefletcher.org/2013/12/pipelining-with-opera...). The lisp version has the extra advantage that you don't have to repeat it between all the intermediate functions. ((->> 2 (* 100) str count) vs 2 |> (* 100) |> str |> count).
I understand how a lisp implementation would work here to require only the single operator (I'm assuming a fairly simple macro).

Would it not be possible to do something similar in another functional language to take a <pipe function> and apply it sequentially to a list of function calls?

There are no semantic problems with this, but typing will get in the way: you can express it fairly easily if all the functions have the same type (such as Int -> Int): actually it's just 'foldr ($)'. But it is difficult to type a list of functions such as each member's return value has the same type as the next one's parameter (symbolically, [an-1 -> an, ..., a1 -> a2, a0 -> a1]). It's easier to refer to the composition of such functions, which is why you would see it as 'h . g . f'.
|> from scalaz. API is not that usable though. F# have List.map, List.filter functions for example, which are not present in scala.
It doesn't seem to have worked out for F# as they have recently adopted what Scala has been doing for years.
So many times this. I've seen some university projects where every coder used eclipse, but no one used its auto format function.