2
votes

I still do not fully understand the difference between the two Clojure arrow macros thread-first -> and thread-last macro ->>. While reading through https://clojure.org/guides/threading_macros I understand that thread first -> is an alternative expression for nested operations on a single object, each datastep using the object as input argument, performing an independent operation.

(defn transformation [object]
  (transform2 (transform1 object)))

becomes

(defn transformation [object]
  (-> object
      (transform1)      ;; object as implicit argument
      (transform2)))    ;; object as implicit argument 

When using threat-last ->> operator, each tranformation uses output of the preceding transformation as implicit argument:

(defn transformation [object]
  (->> object
       (transform1)      ;; object as implicit argument
       (transform2)))    ;; (transform1 object) as implicit argument

What are the practical implicantions of these differences? I understand that it makes sense to use threat -first -> for operations on maps and dictionaries, where each transformation creates a new copy of the original instance, which has to be supplied for the next operation. However, in many cases, both operators actually behave the same:

(->> [2 5 4 1 3 6] (reverse) (rest) (sort) (count)) ;; => 5
(->  [2 5 4 1 3 6] (reverse) (rest) (sort) (count)) ;; => 5
3
Stuart Sierra has a post called threading with style that has good guidance on the usage of the threading macros. - Frank Henard

3 Answers

3
votes

Following up on as-> as an answer purely because I can't format code in a comment.

Here's a usage of as-> from our codebase at work:

                    (-> date
                        (.getTime)
                        (as-> millis
                          (/ (- (.getTime (java.util.Date.)) millis)
                             1000 60 60 24))
                        (long)
                        (min days))

That computation could be unrolled, and ->> used to place the threaded value at the end of the - expression, but would probably be harder to read. It would also be possible to unroll it in such a way that -> alone would be enough (by negating the threaded value and then adding it to (.getTime (java.util.Date.))) but that would make it even harder to read I think.

2
votes

The practical difference is where the macro "inserts" the variable (and the subsequent results) into the expressions:

(ns so.example)

(defn example-1 [s]
  (-> s
      (str "foo")))

(defn example-2 [s]
  (->> s
      (str "foo")))

(example-1 "bar")
;=> "barfoo"

(example-2 "bar")
;=> "foobar"

So (-> "bar" (str "foo")) is the same as (str "bar" "foo") and (->> "bar" (str "foo")) is the same as (str "foo" "bar"). With unary functions -> and ->> do the same thing.

When you need more flexibility as to where these results should be inserted, you would use as->:

(ns so.example)

(defn example-3 [s]
  (as-> s v
      (str "foo" v "foo")))

(example-3 "bar")
;=> "foobarfoo"
1
votes

I think there is a misunderstanding on your part on how -> works.

You say

  1. -> is an alternative expression for nested operations on a single object, each datastep using the object as input argument, performing an independent operation.

and then about ->> you say

  1. When using threat-last ->> operator, each tranformation uses output of the preceding transformation as implicit argument

but statement 1 is not true, and statement 2 is true for both, -> and ->>. This is very easy to test, like this:

cljs.user=> (-> [2 5 4 1 3 6] reverse rest)
(3 1 4 5 2)
cljs.user=> (-> [2 5 4 1 3 6] rest reverse) 
(6 3 1 4 5)

If you add sort to the end of the call chain, like in your example, you wouldn't notice this difference, because both results would be sorted.

Like cfrik said, when you are passing only one argument to a function, then the first argument and the last argument are the same (because there's just one), so that's why it's easy to get confused when all the functions in your call chain accept only one argument, which is the case with your example where you have used count, sort, rest, and reverse.

Another thing you might have missed from the documentation in https://clojure.org/guides/threading_macros is the fact that many functions that work with sequences, like filter, map, and reduce, take the sequence as the last argument as a convention, and that makes it possible to chain calls to them using ->>, like

(->> (range 10) 
     (filter even?)
     (map #(+ 3 %))
     (reduce +))

which becomes

(reduce + (map #(+ 3 %) (filter even? (range 10))))

whereas -> is more suitable for functions like assoc and update, which (like you said) work with single objects and take the object as their first argument (and "transformations/update" on that object as the rest of the arguments), so then you can do the following

(-> person
    (assoc :hair-color :gray)
    (update :age inc))

which becomes

    (update (assoc person :hair-color :gray) :age inc)

To better understand how macros work, try using macroexpand-1, like this

user> (macroexpand-1 '(->> (range 10) 
                      (filter even?)
                      (map #(+ 3 %))
                      (reduce +)))
(reduce + (map (fn* [p1__21582#] (+ 3 p1__21582#)) (filter even? (range 10))))

The argument macroexpand-1 should be a quoted version of the function call, and the result will be the expanded function call.