Reputation: 662
I'm running into some limitations of Clojure macros. I wonder how to optimize the following code?
(defmacro ssplit-7-inefficient [x]
(let [t 7]
;; Duplicated computation here!
`(do [(first (split-with #(not (= '~t %)) '~x))
(drop 1 (second (split-with #(not (= '~t %)) '~x)))])))
(ssplit-7-inefficient (foo 7 bar baz))
;; Returns: [(foo) (bar baz)]
Here are some approaches that don't work:
(defmacro ssplit-7-fails [x]
(let [t 7]
`(do ((fn [[a b]] [a (drop 1 b)]) (split-with #(not (= '~t %)) '~x)))))
(ssplit-7-fails (foo 7 bar baz))
;; Error: Call to clojure.core/fn did not conform to spec.
(defmacro ssplit-7-fails-again [x]
(let [t 7]
`(do
(let [data (split-with #(not (= '~t %)) '~x)]
((fn [[a b]] [a (drop 1 b)]) data)))))
(ssplit-7-fails-again (foo 7 bar baz))
;; Error: Call to clojure.core/let did not conform to spec.
Upvotes: 1
Views: 270
Reputation: 9865
It is not so easy to reason about macros in Clojure - (in my view macroexpand-1
alienates the code a lot - in contrast to Common Lisp's macroexpand-1
...).
My way was first to build a helper function.
(defn %split-7 [x]
(let [y 7]
(let [[a b] (split-with #(not= y %) x)]
[a (drop 1 b)])))
This function uses destructuring so that the split-with
is "efficient".
It does nearly exactly what the macro should do. Just that one has to quote the argument - so that it works.
(%split-7 '(a 7 b c))
;;=> [(a) (b c)]
From this step to the macro is not difficult.
The macro should just automatically quote the argument when inserting into the helper function's call.
(defmacro split-7 [x]
`(%split-7 '~x))
So that we can call:
(split-7 (a 7 b c))
;; => [(a) (b c)]
Using this trick, even generalize the function to:
(defn %split-by [x y]able like this
(let [[a b] (split-with #(not= y %) x)]
[a (drop 1 b)]))
(defmacro split-by [x y]
`(%split-by '~x ~y))
(split-by (a 7 b c) 7)
;; => [(a) (b c)]
(split-by (a 7 b c 9 d e) 9)
;; => [(a 7 b c) (d e)]
The use of (helper) functions in the macro body - and even other macros - or recursive functions or recursive macros - macros which call other macros - shows how powerful lisp macros are. Because it shows that you can use the entirety of lisp when formulating/defining macros. Something what most language's macros usually aren't able to do.
Upvotes: 2
Reputation: 7568
Note that split-with
splits only once. You can use some destructuring to get what you want:
(defmacro split-by-7 [arg]
`((fn [[x# [_# & z#]]] [x# z#]) (split-with (complement #{7}) '~arg)))
(split-by-7 (foo 7 bar baz))
=> [(foo) (bar baz)]
In other use cases, partition-by
can be also useful:
(defmacro split-by-7 [arg]
`(->> (partition-by #{7} '~arg)
(remove #{[7]})))
(split-by-7 (foo 7 bar baz))
=> ((foo) (bar baz))
Upvotes: 2