1
votes

I'm writing a macro to allow pass the clauses as a parameter to functions:

(defmacro parse-cmd [command & body]
  (let [parts (str/split command #" ")
         cmd (first parts)
         args (into [] (rest parts))
         clauses (partition 2 2 body)]
    `(case ~cmd
        ~@(mapcat (fn [c] [(nth c 0) `(apply ~(nth c 1) ~args)]) clauses))))

(defn mysum [a b]
  (+ (Integer. a) (Integer. b)))

(parse-cmd "SET 1 1" "SET"  "GET" println)
2

This works well when cmd is a string, however with a var:

(def cmd "SET 1 1")
(parse-cmd cmd "SET"  "GET" println)

I get ClassCastException clojure.lang.Symbol cannot be cast to java.lang.CharSequenceq clojure.string/split (string.clj:222)

I guess I should prevent the evaluation of the let too, but I can't make it work:

(defmacro parse-cmd [command & body]
  `(let [parts# (str/split ~command #" ")
         cmd# (first parts#)
         args# (into [] (rest parts#))
         clauses# (partition 2 2 ~body)]
     (case cmd#
        (mapcat (fn [c#] [(nth c# 0) `(apply ~(nth c# 1) args#)]) clauses#))))

With this definition I get: ClassCastException java.lang.String cannot be cast to clojure.lang.IFn kvstore.replication/eval12098 (form-init7453673077215360561.clj:1)

1
You get the Symbol cannot be cast to CharSequence exception because cmd, your first argument you give is not a string. Macros don't evaluate vars automatically like functions do. Regarding the best solution, you should think about what you're macro should macroexpand into, that will help you figure out the right way. - schaueho

1 Answers

4
votes

let's macroexpand this (for your second macro)

(parse-cmd "SET 1 1" "SET" mysum "GET" println)

it expands to:

(let [parts__31433__auto__ (str/split "SET 1 1" #" ")
      cmd__31434__auto__ (first parts__31433__auto__)
      args__31435__auto__ (into [] (rest parts__31433__auto__))
      clauses__31436__auto__ (partition
                               2
                               2
                               ("SET" mysum "GET" println))]
  (case
    cmd__31434__auto__
    (mapcat
      (fn [c__31437__auto__] [(nth c__31437__auto__ 0)
                              (seq
                                (concat
                                  (list 'apply)
                                  (list (nth c__31437__auto__ 1))
                                  (list 'args__31432__auto__)))])
      clauses__31436__auto__)))

there are two problems here:

1) you generate this code: ("SET" mysum "GET" println), which obviously causes your exception, because "SET" is not a function

2) you generate the wrong case expression, I see that you have forgotten to unquote-splice your mapcat

Let's try to fix this:

first of all unquote mapcat; then you can move clauses out of your generated let, because it can be totally done at compile-time:

(defmacro parse-cmd [command & body]
  (let [clauses (partition 2 2 body)]
    `(let [parts# (str/split ~command #" ")
           cmd# (first parts#)
           args# (into [] (rest parts#))]
       (case cmd#
         ~@(mapcat (fn [c] [(nth c 0) `(apply ~(nth c 1) args#)]) clauses)))))

now let's check the expansion:

(let [parts__31653__auto__ (str/split "SET 1 1" #" ")
      cmd__31654__auto__ (first parts__31653__auto__)
      args__31655__auto__ (into [] (rest parts__31653__auto__))]
  (case
    cmd__31654__auto__
    "SET"
    (apply mysum args__31652__auto__)
    "GET"
    (apply println args__31652__auto__)))

ok. looks better. let's try to run it:

(parse-cmd "SET 1 1" "SET" mysum "GET" println)

we have another error now:

CompilerException java.lang.RuntimeException: Unable to resolve symbol: args__31652__auto__ in this context, compiling:(*cider-repl ttask*:2893:12)

so expansion also shows us this:

args__31655__auto__ (into [] (rest parts__31653__auto__))
...
(apply mysum args__31652__auto__)

so there are different symbols for args# here. That's because the scope of the generated symbol name is one syntax-quote. So inner syntax-quote with apply generates the new one. You should use gensym to fix that:

(defmacro parse-cmd [command & body]
  (let [clauses (partition 2 2 body)
        args-sym (gensym "args")]
    `(let [parts# (str/split ~command #" ")
           cmd# (first parts#)
           ~args-sym (into [] (rest parts#))]
       (case cmd#
         ~@(mapcat (fn [c] [(nth c 0) `(apply ~(nth c 1) ~args-sym)]) clauses)))))

ok now it should work properly:

ttask.core> (parse-cmd "SET 1 1" "SET" mysum "GET" println)
2
ttask.core> (parse-cmd cmd "SET" mysum "GET" println)
2

great!

I would also recommend you to use destructuring in a mapcat function and quoted let, to make it more readable:

(defmacro parse-cmd [command & body]
  (let [clauses (partition 2 2 body)
        args-sym (gensym "args")]
    `(let [[cmd# & ~args-sym] (str/split ~command #" ")]
       (case cmd#
         ~@(mapcat (fn [[op fun]] [op `(apply ~fun ~args-sym)]) clauses)))))

But if it's not just an exercise in writing macros, you shouldn't use the macro for that, since you pass just string and function references here, so anyway you shall evaluate everything in runtime.

(defn parse-cmd-1 [command & body]
  (let [[cmd & args] (str/split command #" ")
        commands-map (apply hash-map body)]
    (apply (commands-map cmd) args)))