简体   繁体   中英

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.

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM