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:
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
condparts. I'm not very happy with the generalized version's length, and it's kind of ugly. The need for
prev-symis 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 call
vecon each var-arg list prior to giving them to
my-general-condis 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))))