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
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"

• Just as a side note, something like this already exists in the standard library. – Sam Estep Nov 14 '16 at 12:13
• @SamEstep Oh, that's cool. Never seen that one before. Thanks – Carcigenicate Nov 14 '16 at 12:20

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.

• Can't believe I missed just con`ing the inserted element! Good suggestions, thanks. – Carcigenicate Nov 15 '16 at 12:26