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"
-
1\$\begingroup\$ Just as a side note, something like this already exists in the standard library. \$\endgroup\$Sam Estep– Sam Estep2016年11月14日 12:13:20 +00:00Commented Nov 14, 2016 at 12:13
-
\$\begingroup\$ @SamEstep Oh, that's cool. Never seen that one before. Thanks \$\endgroup\$Carcigenicate– Carcigenicate2016年11月14日 12:20:11 +00:00Commented Nov 14, 2016 at 12:20
1 Answer 1
I'd make a few modest changes.
- Don't use the helpers: use
even?
andcount
instead. - Reorder the arguments to
insert-at
to comply with those ofassoc
. - 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.
-
\$\begingroup\$ Can't believe I missed just
con
ing the inserted element! Good suggestions, thanks. \$\endgroup\$Carcigenicate– Carcigenicate2016年11月15日 12:26:51 +00:00Commented Nov 15, 2016 at 12:26