Using `as->` for readability?

I used to think of as-> as something I’d reach for when I needed to thread something into a middle position, but I just realized that it could just be used to add names to intermediate results, a bit like let, except I feel it might even be more readable.

What do you all think?

(-> (process payload)
    (as-> customer (get-cart customer))
    (as-> cart (checkout cart)))

vs

(-> (process payload)
    (get-cart)
    (checkout))

vs

(let [customer (process payload)
      cart (get-cart customer)]
  (checkout cart))

It reminds me a bit of SQL, and I feel it reads pretty nicely.

7 Likes

It’s worth noting that, at least on CLJS, the generated code might be a tad different.

From https://app.klipse.tech, (which I assume is not using optimisations :advanced):

Let

(let [a 1
      b (inc a)
      c (dec b)]
 (str c))
var a_103 = (1);
var b_104 = (a_103 + (1));
var c_105 = (b_104 - (1));
cljs.core.str.cljs$core$IFn$_invoke$arity$1(c_105);

as->

(-> 1
    (as-> a (inc a))
    (as-> b (dec b))
    (as-> c (str c)))
var c_106 = (function (){var b = (function (){var a = (1);
                                              return (a + (1));
                                             })();
                         return (b - (1));
                        })();
cljs.core.str.cljs$core$IFn$_invoke$arity$1(c_106);
3 Likes

I’ve never used as-> but the way you show it there is nice; it provides the readability-benefits I sometimes want from static typing. I’ll have to try it

It’s also worth pointing out how this relates to the discussion to comp or not to comp: when the readability benefits would be significant, this would be a great time “not to comp” and so minimize the “black box” effect.

If the chain of functions has only a single arg, I use -> If it is anything else, I always prefer to be explicit about how things are threaded, using it-> from the Tupelo library: https://github.com/cloojure/tupelo#literate-threading-macro

(it-> 1
      (inc it)                                  ; thread-first or thread-last
      (+ it 3)                                  ; thread-first
      (/ 10 it)                                 ; thread-last
      (str "We need to order " it " items." )   ; middle of 3 arguments
;=> "We need to order 2 items." )
1 Like

Suggestion:

(-> (process payload)
    (get-cart #_ customer)
    (checkout #_ cart))

Possibly play with spacing and the type of the commented form.

2 Likes

Hum, that’s an interesting one as well, never saw it before.

It reminds me a bit of:

(-> (process payload)
    (get-cart ,)
    (checkout ,))

But instead of just documenting the position, it also names it.

I think I don’t find it as readable though, because mentally I already read get-cart, and so I lost the context of the previous form, so now when I see customer I have to mentally backtrack like oh okay process returned a customer.

Hum, though I wonder:

(-> #_customer (process payload)
    #_cart     (get-cart #_customer)
    #_success  (checkout #_cart))
1 Like

Why not just as-> instead?

(as-> 1 it
      (inc it)                                  ; thread-first or thread-last
      (+ it 3)                                  ; thread-first
      (/ 10 it)                                 ; thread-last
      (str "We need to order " it " items." )   ; middle of 3 arguments
;=> "We need to order 2 items." )

Although I would still put it in a regular -> pipeline and avoid the “weird” value/binding order of as->:

(-> 1
    (as-> it
          (inc it)                                  ; thread-first or thread-last
          (+ it 3)                                  ; thread-first
          (/ 10 it)                                 ; thread-last
          (str "We need to order " it " items." )   ; middle of 3 arguments
;=> "We need to order 2 items." ))

(edited to put tail expressions inside as->)

1 Like

That second example doesn’t work for me. Think you meant:

(-> 1
    (as-> it
      (inc it)                                  ; thread-first or thread-last
      (+ it 3)                                  ; thread-first
      (/ 10 it)                                 ; thread-last
      (str "We need to order " it " items." )))   ; middle of 3 arguments
;=> "We need to order 2 items." )

Yup. I’ve updated my code example, thanks.

Reasons:

  1. It has always seemed to me that the placeholder symbol should be the first argument, not the second. I kept getting it backwards until I finally remembered to reverse them from what they “should” be.

  2. There is often no good name for the placeholder symbol. The English pronoun it is perfect name for something that changes with each successive function.

  3. The Groovy programming language already uses it as a default variable name for single-arg functions, and I was familiar with this ideom. So, I decided to re-use a good idea that already existed.

Alan

Well, as-> is designed to be used inside -> so you normally would never provide both the value and the symbol, and it is a perfectly good symbol to use:

(-> value
    (as-> it
          (... it ...)))

Seems weird to me, to bring in a library, to avoid using something that is already in core (especially after so many people clamored for so many years for a threading macro like this).

To be fair, it triggers my OCD that you’ve got this extra weird indentation and added parenthesis nesting. That’s why I also have a -<> macro I use for myself. But, I also hate that, because bringing in a library just for that also bugs me.

So I kinda ended just using let instead for those cases.

But now I’m thinking I can use as-> selectively, so in your example I could do:

(-> 1
    (inc)
    (+ 3)
    (as-> n (/ 10 n))
    (as-> n (str "We need to order " n " items." )))

And ya, I think I like duplicating the as->, even though I know I could just do:

(-> 1
    (inc)
    (+ 3)
    (as-> n (/ 10 n)
            (str "We need to order " n " items." )))

Not too sure yet, I’ll see which I eventually prefer.

That would probably be my preference too: use -> as “normal” and only introduce a symbol where it is actually needed.

I feel like one ends up with things like this:

(-> (process payload) ; payload -> customer 
     get-cart         ; customer -> cart
     checkout)        ; success?

… because the functions are ambiguously named. Another approach is to name each function in a way that helps you understand at a glance what it does:

(-> (payload->customer payload)
    customer->cart
    checkout)
6 Likes