_> some_> threading macros for clojure.
;; in deps.edn
{:deps {github-akovantsev/archery
{:git/url "https://github.com/akovantsev/archery"
:sha "somegitsha"}}}(ns foo.bar.baz
(:require
[com.akovantsev.archery :refer [some_> _> _]]))Or, if you are feeling extra adventurous:
(ns foo.bar.baz
(:refer-clojure :exclude [-> ->> some-> some->>])
(:require
[com.akovantsev.archery :refer [some_> _> _] :rename {_> ->, some_> some->}]))Same as some-> for -> but for _>. All rules and gotchas are the same as for _>.
Essentially, _> is a bunch of clojure.core's as-> and -> wrapped into top level ->.
For each top-level-arg-form:
If arg is (_> ...) form or any form without _ – it receives prev form as first arg, as if inside clojure.core ->*.
If arg contains _ symbol – it gets wrapped into clojure.core as->: (as-> _ (some [arg form {containing _}])).
Then _> wraps all top-level-arg-forms with clojure.core ->.
That's it.
(_> x foo (bar a) (baz _ b _) (_> _ lel) :quux)
;=>
(clojure.core/-> x
(foo ,) ;; has no _ - gets prev as first arg
(bar , a) ;; has no _ - gets prev as first arg
(clojure.core/as-> _ (baz _ b _)) ;; has _ and is not _> - gets wrapped with as->
(_> , _ lel) ;; is _> - gets prev as first arg
(:quux ,)) ;; has no _ - gets prev as first arg
*There was a different design option:
Thread as -> only into scalars (numbers, keywords, symbols, etc), as-> into forms with parens or with _.
It would have forced you to specify _ in all fn calls, which would have:
- saved you some arity-related bugs,
but - it would have prevented several cool features, like
threading into try-catchand reader-macro getters and spy. - and IDE should already hint you about arity error, effectively pushing you to explicitly add
_to forms with parens.
->->>some->some->>cond->cond->>– notice how many already, oof- They implicitly put prev form into next, which is same amount of letters as in onion nested form, but readable, nice
- However each has its own fixed place where prev form goes. This means you need to 1) plan your arrows beforehand, and 2) often rewrite when adding expressions later, oof
- You can nest
->>kin inside->kin, but not vice versa, makes rewrites and planning even more unpleasant, oof - This generates larger git diffs, oof
- This increases nestedness and indentation level, however, conceptually you are still threading, oof
- They wrap "scalar" forms-without-parens with parens, e.g.
(-> x f)->(-> x (f))->(f x), nice - None of those above can thread into some middle arg position
(f a _ b), butas->can, nice - But that's another one to plan and rewrite, oof
- And it can't be transitioned into from
->>kin, oof - So maybe lets just use only
as->? But it requires you to always specify placeholder. Tedious, but it is good for readability and maintenance, ok - But it can't auto-wrap scalars with parens, and what is THAT?
(as-> 1 _ inc) => #object[clojure.core$inc], big oof - Brevity is very important:
(-> x foo bar baz)vs(as-> x _ (foo _) (bar _) (baz _), oof again - However it can fork
(as-> x _ [_ _]), nice cond->kin can'telseoof and thread into predicates, oof- But
as->can do both:(as-> x _ (if (pred _) (then _) (else _)), nice, nice - It can sideeffects:
(as-> x _ (do (print _) _)), nice - Other prior art on the internet:
when->arg->for->when->let->fn->-<>-?<>-<><:p-?>-?>>.?.:DDD that's 12 oofs just to match amount, imagine combinatorial explosion while trying to compose em, OOF - Designing and memorizing which function fits which macro:
->or->>?, oof - Trying to fit expr into
->or->>by changing args order by wrapping partial-call-expressions in lambdas, oof (but remember, you can't nest#(), oof) - Forgetting change
(or x)when refactoring from(-> x foo bar (or y))to(->> x (foo a) bar (or y)), oof ->and->>can't thread into coll literals to wrap things:(-> x [])is([] x)not[x], oof- Only
as->can thread into coll literals:(as-> x _ [{:a _}])=>[{:a x}], nice, but is rarely used due to overall verbocity, oof
- Autowrapping scalars with parens, nice,
(_> x foo bar baz :a) - Threading as
->by default, preserving at least some implicitness, nice - Default fixed standard placeholder
_, no need to come up with new one and type it out everytime, nice - Can't change it though, ok
- Autowrapping form containing
_withas->, so entire form works like giantas->: - Gives us forking
(_> x [_ _]), nice - Gives us
else, nice,dynamic preds, nice,(_> x (if (pred _) #{_} [_]) - Sideeffects
(_> x (do (print _) _)), nice - Any arg position, nice, and any number of arg positions, nice,
(_> x (f a b c _ d _)) - No more need for 666 other, barely composable threading macros, nice
- No need to wrap in lambdas just to adjust arg positions for current
->or->>, nice - Explicit
(or _ default), nice - No need in extra nesting and indentation, just keep threading! nice
- No need for plumbing planning and rewriting when pipeline steps change, nice
- Collecting checkpoints a la reductions with forking and nested
_>:(_> 1 inc [_ (_> _ (+ 2) (* 3)]) => [2 12], nice (/ _ x)(< x _ y)are now explicit and don't suffer from->to->>refactorings, just like(or _ x), nice- All those extra
_after inline map lambdas(map long-ass-multi-line-lambda _), oof - Threadable
for!(_> x (for [a _] [a _ a]) ...), nice - Threadable
reducewhichever way you want!(_> x (reduce f _ ys) ...),(_> x (reduce f r _) ...), nice - Wrapping with literals
(_> x [:a _])=>[:a x]now literally looks like its output, nice! - You might think, the only missing thing now is threadable unwrapping which looks like input... I got you: https://github.com/akovantsev/destruct
;; data-reader.clj
{g com.akovantsev.archery/data-reader-for-getter}(_> {"a" {\b [:c "d"]}} #g"a" #g\b (group-by keyword? _) #g true #g 0)
;=>
(_>
{"a" {\b [:c "d"]}}
((com.akovantsev.archery/-mg "a"))
((com.akovantsev.archery/-mg \b))
(group-by keyword? _)
((com.akovantsev.archery/-mg true))
((com.akovantsev.archery/-mg 0)))
;=>
:cNo behavior inheritance between _> some_>, just like between core -> and some->: inner -> is opaque for outer some->, and vice versa.
(_> nil
(some_> name) ;; short circuit before name throws NullPointerException
(/ 0)) ;; but proceeds right into Divide by zeroWatch out for "ignore bind" _ inside bindings inside _>
(clojure.walk/macroexpand-all
'(_> [1 2 3] (let [[x y _] _] [x y _]) str/join))
;; 1 2 1 2
;=>
(str/join
(let* [_ [1 2 3]]
(let* [vec__23224 _
x (clojure.core/nth vec__23224 0 nil)
y (clojure.core/nth vec__23224 1 nil)
_ (clojure.core/nth vec__23224 2 nil)] ;;overridden
[x y _])))
;=>
"123"
; 2
;;vs:
(clojure.walk/macroexpand-all
'(_> [1 2 3] (let [[x y _z] _] [x y _]) str/join))
;; 1 2 1 1
;=>
(str/join
(let* [_ [1 2 3]]
(let* [vec__23248 _
x (clojure.core/nth vec__23248 0 nil)
y (clojure.core/nth vec__23248 1 nil)
_z (clojure.core/nth vec__23248 2 nil)]
[x y _])))
;=>
"12[1 2 3]"
; 1Only -> always expands "thread-first". It is easy to forget as-> and some-> expand defferently.
;; core -> expands this with everything inside of try, wicked but handy!
(clojure.walk/macroexpand-all
'(-> 1 str inc (try (catch Exception e :caught))))
;; ^ ^
;=>
(try
(inc (str 1))
(catch Exception e :caught))
;=>
:caught
;; but core some-> and as-> expand this outside, rendering try - noop:
(clojure.walk/macroexpand-all
'(some-> 1 str inc (try (catch Exception e :caught))))
;; ^ ^
(let* [G__2306 1
G__2306 (if (nil? G__2306) nil (str G__2306))
G__2306 (if (nil? G__2306) nil (inc G__2306))]
;; ^ throws outside of try
(if (clojure.core/nil? G__2306)
nil
(try
G__2306
(catch Exception e :caught))))
(clojure.walk/macroexpand-all
'(as-> 1 _ (str _) (inc _) (try _ (catch Exception e :caught))))
;; ^ ^
;=>
(let* [_ 1
_ (str _)
_ (inc _)] ;;throws here
(try _ (catch Exception e :caught)))
;=>
ClassCastException: java.lang.String cannot be cast to class java.lang.Number
;; recall, _> and some_> wrap form in as-> only if it contains _,
;; so this behaves like core -> (note NO _ inside try):
(clojure.walk/macroexpand-all
'(_> 1 str inc (try (catch Exception e :caught))))
;; ^
;=>
(try
(inc (str 1))
(catch Exception e :caught))
;=>
:caught
;; and this behaves just like core as-> (note _ inside try):
(clojure.walk/macroexpand-all
'(_> 1 str inc (try _ (catch Exception e :caught))))
;; ^
;=>
(-> 1
str
inc ;;throws here
(as-> _ (try _ (catch Exception e :caught)))
;=>
(as-> (inc (str 1)) _ ;;throws here
(try _ (catch Exception e :caught)))
;=>
(let* [_ (inc (str 1))] ;;throws here
(try _ (catch Exception e :caught)))
;=>
ClassCastException