2
\$\begingroup\$

To help myself learn macros, I made a custom version of the threading macro that lets you choose which argument the "thread" gets put into.

My main concern is simplifying it, and making it more idiomatic, but anything notable is welcome. If anyone knows of a better implementation for insert-at, I'd love to know that as well!

; Helpers. Other macros use these, or else I'd just inline them
(defn factor? [n m]
  (= (rem n m) 0))

(defn mult-forms? [n forms]
  (factor? (count forms) n))

(defn insert-at
  "Inserts an element at the given index. Returns a list.
  Indices out of range of the collection are treated as either 0 or (dec (count xs))
  Likely very expensive."
  [xs x index]
  (let [[f-half s-half] (split-at index xs)]
    (concat f-half `(~x) s-half)))

(defmacro n->
  "Expects a value to be threaded, and pairs of indices and forms.
  The indices indicate which index the value should be inserted at in the following form.
  Example:
      (n-> 1
          1 (+ 2)
          0 (+ 3))
  Expands to: (+ (+ 2 1) 3)"
  [val & forms]
  (if (and (not (empty? forms))
           (mult-forms? 2 forms))
    (let [[thread-n form & rest-forms] forms
          threaded  (insert-at form val (inc thread-n))]
      `(n-> ~threaded ~@rest-forms))
    val))

Note, I'm not claiming this is a useful macro; it will likely lead to confusing to read code. This was just an exercise.

Example:

(n-> "Hello"
  0 (str "0" "1" "2" "3")
  1 (str "0" "1" "2" "3")
  2 (str "0" "1" "2" "3"))

Expands to:

(clojure.core/str "0" "1"
    (clojure.core/str "0" 
        (clojure.core/str "Hello" "0" "1" "2" "3") "1" "2" "3") "2" "3")

Which evaluates to:

"010Hello012312323"
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Just as a side note, something like this already exists in the standard library. \$\endgroup\$
    – Sam Estep
    Nov 14, 2016 at 12:13
  • \$\begingroup\$ @SamEstep Oh, that's cool. Never seen that one before. Thanks \$\endgroup\$ Nov 14, 2016 at 12:20

1 Answer 1

1
\$\begingroup\$

I'd make a few modest changes.

  • Don't use the helpers: use even? and count instead.
  • Reorder the arguments to insert-at to comply with those of assoc.
  • Make the even-ness condition in n-> an assertion: don't try to deal with invalid cases.

Thus

(defn insert-at [xs n x]
  (let [[f-half s-half] (split-at n xs)]
    (concat f-half (cons x s-half))))

(Note the use of cons)

For example,

(insert-at [1 2 3 4 5] 3 :a)
;(1 2 3 :a 4 5)

And

(defmacro n-> [val & forms]
  (assert (even? (count forms)))
  (if (seq forms)
    (let [[n form & rest-forms] forms
          threaded  (insert-at form (inc n) val)]
      `(n-> ~threaded ~@rest-forms))
    val))

For example,

(macroexpand '(n-> 1, 1 (+ 2), 0 (+ 3)))
;(+ (+ 2 1) 3)

Note that the macro now works (as an identity) with one argument:

(macroexpand '(n-> (whatever you like)))
;(whatever you like)

Another approach is to do the work of the macro inside a function:

(defn n->fn [val & forms]
  (assert (even? (count forms)))
  (let [pairs (partition 2 forms)]
    (reduce
     (fn [acc [n form]]
       (insert-at form (inc n) acc))
     val
     pairs)))

For example,

(apply n->fn '(1, 1 (+ 2), 0 (+ 3)))
;(+ (+ 2 1) 3)

Once this function gets hold of the forms as data, it can do what the macro did without implicit recursion.

So we can define the macro in terms of the function:

(defmacro n-> [& stuff]
  (apply n->fn stuff))

... with identical results.

\$\endgroup\$
1
  • \$\begingroup\$ Can't believe I missed just coning the inserted element! Good suggestions, thanks. \$\endgroup\$ Nov 15, 2016 at 12:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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