Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make this macro variadic in clojure?

Tags:

macros

clojure

I want to make something called ds so that

(let [a 2]
  (ds a))

->

 "a->2"

and

(let [a 1 b 2 c 3]
    (ds a b c)) 

->

 "a->1, b->2, c->3"

And so far I've got as far as:

(defmacro ds3 [a b c] 
     `(clojure.string/join ", " 
          [(str '~a "->" ~a) 
           (str '~b "->" ~b) 
           (str '~c "->" ~c)]))

Which seems to work:

 (let [ a 1 b 2 c 3]
     (ds3 a b c)) ; "1->1, 2->2, 3->3"

Obviously I can define ds1 ds2 ds3 etc..., but I wondered how to make it variadic?

like image 520
John Lawrence Aspden Avatar asked Feb 04 '13 12:02

John Lawrence Aspden


3 Answers

Here you go:

(defmacro ds [& symbols]                                                                                                                             
  `(clojure.string/join ", "                                                                                                                         
                        ~(into [] 
                           (map (fn [s] `(str ~(name s) "->" ~s))  symbols))))                                                                
like image 84
Ankur Avatar answered Nov 20 '22 05:11

Ankur


Ankur's answer is probably the most practical, but he is deferring a lot of the work to runtime which could be done at macroexpansion time. It's a useful exercise, and a nice demonstration of the power macros can bring, to see how much of the work you can do at compile time:

(defmacro ds [& args]
  `(str ~(str (name (first args)) "->")
        ~(first args)
        ~@(for [arg (rest args)
                clause [(str ", " (name arg) "->") arg]]
            clause)))

(macroexpand-1 '(ds a b c))
=> (clojure.core/str "a->" a ", b->" b ", c->" c)

This avoids building any temporary objects at runtime, and does the absolute minimum number of string concatenations.

like image 24
amalloy Avatar answered Nov 20 '22 04:11

amalloy


EDIT:

Thanks to suggestions by @amalloy, here are some improved macros that don't use the 'badly wrong' eval and include some mini-tests:

(import 'java.lang.ArithmeticException)

(defmacro explain-expr
  "Produce a string representation of the unevaluated expression x, concatenated to
  an arrow and a string representation of the result of evaluating x, including
  Exceptions should they arise."
  [x]
  `(str ~(str x) " ~~> "
        (try ~x (catch Exception e# (str e#)))))

(println (explain-expr (* 42 42)))
(println (explain-expr (let [x 1] x)))
(println (explain-expr (/ 6 0)))
(println (let [x 1] (explain-expr x)))
(let [y 37] (println (explain-expr (let [x 19] (* x y)))))
(let [y 37] (println (explain-expr (let [y 19] (* y y)))))
(* 42 42) ~~> 1764
(let [x 1] x) ~~> 1
(/ 6 0) ~~> java.lang.ArithmeticException: Divide by zero
x ~~> 1
(let [x 19] (* x y)) ~~> 703
(let [y 19] (* y y)) ~~> 361
(defmacro explain-exprs
  "Produce string representations of the unevaluated expressions xs, concatenated
  to arrows and string representations of the results of evaluating each
  expression, including Exceptions should they arise."
  [& xs]
  (into [] (map (fn [x]
                  `(str ~(str x) " ~~> "
                        (try ~x (catch Exception e# (str e#)))))
                xs)))

(clojure.pprint/pprint
 (let [y 37]
   (explain-exprs
    (* 42 42)
    (let [x 19] (* x y))
    (let [y 19] (* y y))
    (* y y)
    (/ 6 0))))
["(* 42 42) ~~> 1764"
 "(let [x 19] (* x y)) ~~> 703"
 "(let [y 19] (* y y)) ~~> 361"
 "(* y y) ~~> 1369"
 "(/ 6 0) ~~> java.lang.ArithmeticException: Divide by zero"]
(defmacro explanation-map
  "Produce a hashmap from string representations of the unevaluated expressions
  exprs to the results of evaluating each expression in exprs, including
  Exceptions should they arise."
  [& exprs]
  (into {}
        (map (fn [expr]
               `[~(str expr)
                 (try ~expr (catch Exception e# (str e#)))])
             exprs)))

(clojure.pprint/pprint
 (let [y 37]
   (explanation-map
    (* 42 42)
    (let [x 19] (* x y))
    (let [y 19] (* y y))
    (* y y)
    (/ 6 0))))
{"(* 42 42)" 1764,
 "(let [x 19] (* x y))" 703,
 "(let [y 19] (* y y))" 361,
 "(* y y)" 1369,
 "(/ 6 0)" "java.lang.ArithmeticException: Divide by zero"}

DEPRECATED:

I'm leaving this in as an illustration of what not to do.

Here's a variation that will work on any kind of expression (I think)

(defmacro dump-strings-and-values
  "Produces parallel vectors of printable dump strings and values. A dump string
  shows an expression, unevaluated, then a funny arrow, then the value of the
  expression."
  [& xs]
  `(apply map vector ;; transpose
          (for [x# '~xs
                v# [(try (eval x#) (catch Exception e# (str e#)))]]
            [(str x# " ~~> " v#) v#])))

(defmacro pdump
  "Print dump strings for one or more given expressions by side effect; return
  the value of the last actual argument."
  [& xs]
  `(let [[ss# vs#]
         (dump-strings-and-values ~@xs)]
     (clojure.pprint/pprint ss#)
     (last vs#))

Some samples:

(pdump (* 6 7))

prints ["(* 6 7) ~~> 42"] and returns 42.

(pdump (* 7 6) (/ 1 0) (into {} [[:a 1]]))

prints

["(* 7 6) ~~> 42"
 "(/ 1 0) ~~> java.lang.ArithmeticException: Divide by zero"
 "(into {} [[:a 1]]) ~~> {:a 1}"]

and returns {:a 1}.

EDIT:

My attempt to get rid of the outer brackets in the printed output, namely

(defmacro vdump
  "Print dump strings for one or more given expressions by side effect; return
  the value of the last actual argument."
  [& xs]
  `(let [[ss# vs#]
         (dump-strings-and-values ~@xs)]
     (map clojure.pprint/pprint ss#)
     (last vs#)))

does NOT work, and I'm not sure why. It doesn't print output, but the macro expansion looks good. Could be an nREPL or REPL issue, but I gave in and just use the one above and don't worry about the brackets much.

like image 1
Reb.Cabin Avatar answered Nov 20 '22 06:11

Reb.Cabin