0
\$\begingroup\$

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 for prev-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 call vec on each var-arg list prior to giving them to my-general-cond. Since my-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))))
\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Jun 12 '18 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 Jun 12 '18 at 4:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.