I was bored and in a mood to write some macros, so I decided as an exercise to try and remake each of the standard threading macros: ->
, ->>
, some->
, some->>
, as->
, cond->
, cond->>
, and doto
. doto
doesn't seem to be considered a "threading macro", but it's very close to the same idea, so I wrote an implementation of it as well.
Usage examples of each:
(my-> 1
(+ 2)
(* 3)
(- 4)
(/ 5))
=> 1
(my->> 1
(+ 2)
(* 3)
(- 4)
(/ 5))
=> -1
(my-some-> 1
(+ 2)
(* 3)
(println 4)
(/ 5))
9 4
=> nil
(my-some->> 1
(+ 2)
(* 3)
(println 4)
(/ 5))
4 9
=> nil
(my-as-> 1 a
(+ a 2)
(* 3 a)
(- a 4)
(/ a 5))
=> 1
(let [n 10]
(my-cond-> []
(odd? n) (conj "odd")
(even? n) (conj "even")
(zero? n) (conj "zero")
(pos? n) (conj "positive")))
=> ["even" "positive"]
(let [n 10]
(my-cond->> [1 2 3 4 5]
(odd? n) (map #(* % 2))
(even? n) (map #(* % 3))
(zero? n) (map #(* % 4))
(pos? n) (map #(* % 5))))
=> (15 30 45 60 75)
(my-doto (Object.)
(println "A")
(println "B"))
#object[java.lang.Object 0x7f681f0f java.lang.Object@7f681f0f] A
#object[java.lang.Object 0x7f681f0f java.lang.Object@7f681f0f] B
=> #object[java.lang.Object 0x7f681f0f "java.lang.Object@7f681f0f"]
Nearly all of them ended up being simple reductions. I looked over the core
's definitions, and I find them to be "overly explicit". They're only defined 1/4 of the way down the core
though, so that might be contributing to what options he/they had available.
I honestly find my versions to be more readable, but I'm sure there's things that can be improved on. My main concerns are:
My versions don't handle meta data. I don't manually deal with meta data very often, so I may be missing something, but I don't see why meta information would need to be transferred. What meta data does the form itself carry? I would think any relevant data would be attached to the objects inside the form.
Anything to improve the
cond
parts. I'm not very happy with the generalized version's length, and it's kind of ugly. The need forprev-sym
is unfortunate. If there's a way to get rid of it, I'd like to hear it. It's also unfortunate that I need to callvec
on each var-arg list prior to giving them tomy-general-cond
. Sincemy-general-cond
is a plain function, the var-arg list will be evaluated as forms prior to them being passed in, leading to weird errors. I could fix this by making it a macro, but it doesn't need to be a macro, so I'd rather not make it one.Any other changes you think would help!
(ns macros.expr-threading)
(defn- ensure-wrapped [expr]
(if (list? expr)
expr
(list expr)))
(defn- insert-first [arg form]
(let [[f & args] (ensure-wrapped form)]
(apply list f arg args)))
(defn- insert-last [arg form]
(let [w-form (ensure-wrapped form)]
(concat w-form (list arg))))
(defmacro my-> [expr & forms]
(reduce insert-first expr forms))
(defmacro my->> [expr & forms]
(reduce insert-last expr forms))
(defmacro my-as-> [expr sym & forms]
(reduce (fn [prev form]
`(let [~sym ~prev]
~form))
expr
forms))
(defn- my-general-some [macro-sym expr forms]
(reduce (fn [prev form]
`(when-let [res# ~prev]
(~macro-sym res# ~form)))
expr
forms))
(defmacro my-some-> [expr & forms]
(my-general-some 'my-> expr forms))
(defmacro my-some->> [expr & forms]
(my-general-some 'my->> expr forms))
(defn- my-general-cond [macro-sym expr clause-pairs]
(let [prev-sym (gensym)]
(my->> clause-pairs
(partition 2)
(reduce (fn [prev [pred-expr form]]
`(let [~prev-sym ~prev]
(if ~pred-expr
(~macro-sym ~prev-sym ~form)
~prev-sym)))
expr))))
(defmacro my-cond-> [expr & clause-pairs]
(my-general-cond 'my-> expr (vec clause-pairs)))
(defmacro my-cond->> [expr & clause-pairs]
(my-general-cond 'my->> expr (vec clause-pairs)))
(defmacro my-doto [expr & forms]
(let [sym (gensym)
alt-forms (map #(insert-first sym %) forms)]
`(let [~sym ~expr]
(do ~@alt-forms ~sym))))
1 Answer 1
Interesting. I never thought of using a reduce
when writing the it->
macro.
(defmacro it->
[expr & forms]
`(let [~'it ~expr
~@(interleave (repeat 'it) forms)
]
~'it))
Usage:
(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." )
I can see that simply nesting the let
forms instead having one long let
expression might be simpler.
There is more documentation here.
-
\$\begingroup\$ I think the core should have used a standardized symbol like this. Having the extra argument to specify a name seems unnecessary. Nice idea. \$\endgroup\$Carcigenicate– Carcigenicate2018年06月12日 01:13:20 +00:00Commented Jun 12, 2018 at 1:13
-
\$\begingroup\$ I copied the idea from the way Groovy does closures with a default symbol
it
for 1-arg functions. \$\endgroup\$Alan Thompson– Alan Thompson2018年06月12日 04:02:58 +00:00Commented Jun 12, 2018 at 4:02