Search code examples
clojuremacros

fn and let inside clojure macro


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.

Solution

  • 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))