read


read
or learn more

Thrush in Clojure – Redux

Sep 28, 2010

Combinators are fun. In fact, there seems to be a combinator renaissance lately — almost like the Ancient Greeks who excavated Mycenaean technology surpassing their own1. There is one specific combinator called the Thrush that effectively takes a set of nested functions and turns them into a pipeline. There are far better explanations of the Thrush than this, including one by Reginald Braithwaite focusing on Ruby, one by Debasish Ghosh focusing on Clojure, and and another focused on Clojure by Shawn Hoover. Go ahead and read those — I’ll wait…

Of the posts linked above, I tend to point people toward Reginald Braithwaite’s treatment because it focuses in on the functional aspects whereas the latter two do not. What do I mean by that? Well, the Clojure treatments focus too much on the -> and ->> macros which are only mostly Thrush combinators. What a wishy-washy thing to say! Maybe a more direct way to phrase it is that neither -> nor ->> are the Thrush combinator. Eeeek!

A Little Bit About Macros

Macros in Clojure (and Lisp in general) operate in different “times”, the most obvious being “compilation time”. The picture is actually foggier than this, but for this discussion it should suffice. What that means is that a macro receives its arguments as unevaluated data structures that are then manipulated in some way before being fed into the compiler. In the case of the -> macro, a call of the following will look and act just like the Thrush operator mentioned in the posts above:

(-> 5 (+ 4) -)
;=> -9

That is, we take the number 5, feed it into the addition (to four) function, and then negate it. The expansion of this macro call would be:

(- (+ 5 4))
;=> -9

This is what I meant by mostly Thrush.

However, -> performs a transformation of its arguments into a nested form and doesn’t deal directly with functions at all, if it did then we would expect the following to work:

(-> 5 (fn [x] (+ x 4)) -)
; java.lang.RuntimeException: 
;   java.lang.UnsupportedOperationException...

The reason for this exception can be understood by observing the following:

(macroexpand-1 '(-> 5 (fn [x] (+ x 4))))
;=> (fn 5 [x] (+ x 4))

The -> macro is treating the form (fn [x] (+ x 4)) not as a function at all but instead as a list of the symbol fn, a vector [x], and another list (+ x 4) and just plops 5 right between the the symbol and vector. Clearly -> is not the Thrush at all, but does play one on TV.

The Thrush in Clojure

Clojure has a function comp that works similarly to how we want the Thrush to act, but it does so in reverse — so why not fix that “deficiency” directly:

(defn thrush [a & args] 
  ((apply comp (reverse args)) a))

(thrush 5 (fn [x] (+ x 4)) -)
;=> -9

Hooray!! We’ve done it! A real-live Thrush combinator in Clojure. There is one problem however. This implementation is forced to walk the arguments once more than it should. A more elegant and efficient solution (by Chris Houser, author of The Joy of Clojure) is as follows:

(defn thrush [& args] 
  (reduce #(%2 %1) args))

(thrush 5 #(+ % 4) -)
;=> -9

Elegant indeed!

Thrushiness

My goal in this post was not to bash the view of -> and ->> as having a high degree of Thrushiness. Instead, I wanted to illustrate that they are not entirely Thrushful, and you can run into issues if you treat them as such.

:f

Thanks to Craig Andera for reading a draft of this.


  1. This is how I feel whenever I read a computer book from the 70s. 

7 Comments, Comment or Ping

  1. You sir, are a thrush of the highest order :) Great post!

  2. Shawn Hoover

    One thing that helps is if you don’t know what “entirely thrushful” means, you can just use ->>, run macroexpand when you get bit, and be happy. Or you can read this and understand.

    What I discovered from this exercise (other than being rather enlightened by you and chouser) is that whether to use a thrush combinator or ->> for the stuff Raganwald was talking about is a trade-off between very clean use of anonymous fns (you can use them with ->>, but you have to wrap them in an extra set of parens) and very clean use of forms like (filter odd?)–note, no fn required there.

    Nice post. This could make nice material for explaining macros to someone.

  3. David Beckwith

    Thanks a lot for explanation. It helps a lot. If you ask me, -> is retarded.

    BTW, I think thrush:

    (defn thrush [& args] 
      (reduce #(%2 %1) args))
    

    is so important that we should make a monument to it by bestowing it’s own 1-2 character notation like +> or something. (note, the “t” for “thrush”) :)

  4. Nice post.

    There are also implementations of the thrush combinator in not so functional languages. I implemented it in C# http://alicebobandmallory.com/articles/2009/10/06/the-thrush-combinator-in-c and Debasish Ghosh did it in Scala http://debasishg.blogspot.com/2009/09/thrush-combinator-in-scala.html

  5. Scala — a not-so-functional language??

  6. djui

    (-> 5 (#(+ % 4)) -)

  1. albert - Oct 31st, 2010

Reply to “Thrush in Clojure – Redux”