Welcome! Please see the About page for a little more info on how this works.

+4 votes
in Clojure by

I've been using Clojure on and off for hobby stuff for a couple of years, and I've read a couple of books, but just the other day I was reading through the threading macros guide again and finally noticed the as->. Maybe I'm just blind, but I'd swear I've never seen it used in any examples I've read, or highlighted in any books. Is there a big performance hit to using it vs using one of the less flexible threading macros?

I've always enjoyed the experience of building up a data processing pipeline by plopping things into -> or ->> (being able to remove a step for testing by commenting out one line is so nice!), but I get bogged down in getting all the functions I want to play nice with -> or ->>. I confess I've gone as far as wrapping a function to change it's argument order. I've been having such a blast with as-> that it feels like surely there must be some drawback I'm missing. Why isn't this the way to chain functions together?

5 Answers

+13 votes

I'm surprised no one has mentioned that as-> is designed to work inside -> (all the threading macros work inside ->). Going through other people's answers, here's what I'd expect:

(-> 2
    (+ 1)
    (as-> x (* 2 x)) ; or just (->> (* 2))

(-> thing
    (->> (fn2 bleh)) ; or (as-> x (fn2 bleh x))
    (fn3 blah))

As you can see, the argument order of as-> -- expr symbol body -- lends itself to threading, but in both these cases you could simply thread from -> into ->> to have the threaded value at the end of the form.

as-> shines when you have just one or two forms in an otherwise straight-line thread that do not want the threaded value at the beginning or at the end:

(-> thing
    (fn1 arg2 arg3)
    (+ 13)
    (as-> q (fn2 arg1 q arg3))
    (fn3 second-arg))

Or, perhaps, you need a let for destructuring or something:

(-> foo
    (processs-it :expand)
    (as-> m (let [{:keys [start end]} m]
              (- end start)))
I think is useful to point to Stuart Sierra's post about Don't with `as->`: https://stuartsierra.com/2018/07/15/clojure-donts-thread-as

I'm honest that I've read this blog post several times and never really understood why Stuart would prefer to wrap the `(fn ... var ...)` form into his own function only to fix the order of the parameters.
Ah, I should have known that Stuart would have words of wisdom about this -- and also reinforces the "as-> inside ->" point that I made.

His comment about wrapping that irritating function means this:

(defn wrapper [ctx other args ...]
  (irritating other ctx args ...))

And then you can use the wrapper in a straight -> threading expression without needing to use as->
Oh, sorry, I was not clear in my comment. I didn't understand the "why". Why not just embrace the "as-> inside ->" as you both pointed out?
Because simple chains of just -> or of just ->> are easier to read. So writing a simple wrapper for a function might well be the best path to readability, especially if you need to use it in multiple -> chains.
+11 votes

as-> was added in Clojure 1.5 so some of the older intro books probably don't mention it, but it's definitely useful for creating pipelines where functions have mixed order.

There's no performance hit for any of these threading macros because they are macros - they all rewrite the code at compile time so at execution time you're doing pretty much the identical thing you would have by just nesting the functions.

user=> (as-> 2 x (+ x 1) (* 2 x) (range x))
(0 1 2 3 4 5)
user=> (macroexpand-1 '(as-> 2 x (+ x 1) (* 2 x) (range x)))
(clojure.core/let [x 2 x (+ x 1) x (* 2 x)] (range x))

Personally, I rarely reach for as->. In general, either -> or ->> is sufficient for the set of calls I'm making. Because of the general advice on argument order, I'm either doing a series of collection ops, or a series of sequence ops, but usually not a mixture.

+3 votes

Why isn't this the way to chain functions together?

I've wondered the same thing, but to be honest, i use -> and ->> way more than i use as->. I guess it just comes down to brevity. One neat thing about the normal threading macros is, if a function only takes one argument, you can leave out the parens altogether. So

(-> 0 (dec) (* 5) (Math/abs))

can just be written as

(-> 0 dec (* 5) Math/abs)

But yeah you can take it too far. When i find myself writing things like

(->> thing fn1 (fn2 bleh) (#(fn3 % blah)))

i stop coding and go for a walk...

+1 vote

I use it all the time and haven't noticed anything. Also, looking in the code of the macro it seems like a straightforward one, with no drawbacks.

0 votes

as-> is great, but the implementation differs from -> and ->> and can produce different behavior. -> and ->> recursively nest calls, while as-> creates a sequence of let bindings. For example, (-> a long-running future) transforms to (future (long-running a)), while (as-> a $ (long-running $) (future $)) becomes (let [$ a, $ (long-running $)] (future $)) (which does not run quite as asynchronously as it looks).

In practice, I haven't found this to be a problem, but it is a caution about using as-> in certain circumstances.