Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

clojure macro to generate functions

Tags:

clojure

I'm trying to write a macro that will generate n functions. Here's what I have so far:

; only defined this because if I inline this into make-placeholders
; it's unable to expand i# in  ~(symbol (str "_" i#))
(defmacro defn-from [str mdata args & body]
    `(defn ~(symbol str) ~mdata ~args ~@body))

; use list comprehension to generate n functions
(defmacro make-placeholders [n] 
    `(for [i# (range 0 ~n)] (defn-from  (str "_" i#) {:placeholder true} [& args] (nth args i#))))

; expand functions _0 ... _9
(make-placeholders 9)

The error I get is:

java.lang.ClassCastException: clojure.lang.Cons cannot be cast to java.lang.String

And I'm not really sure what that means, but I have this vague notion that (for ...) isn't working the way I think it is inside a macro.

like image 463
Kevin Avatar asked Oct 21 '11 16:10

Kevin


2 Answers

You are getting confused about the distinction between runtime and compile-time, and between macros and functions. Solving a macro problem with eval is never1 the right answer: instead, make sure that you return code that does what you want to happen. Here's a minimal change to make your original version work.

The main changes are:

  1. defn-from is a function, not a macro - you just want a convenient way to create lists, which the main macro is responsible for inserting into the result form. You do not want a macro here, because you don't want it expanded into the body of make-placeholders.

  2. make-placeholders starts with a do, and does its for outside of a syntax-quote. This is the most important part: you want the code returned to the user to look like (do (defn ...)), as if they'd typed it all in by hand - not (for ...), which could only ever def a single function.


(defn defn-from [str mdata args & body]
    `(defn ~(symbol str) ~mdata ~args ~@body))

; use list comprehension to generate n functions
(defmacro make-placeholders [n]
  (cons `do
        (for [i (range 0 n)]
          (defn-from (str "_" i) {:placeholder true}
            '[& args]
            `(nth ~'args ~i)))))

user> (macroexpand-1 '(make-placeholders 3))
(do (clojure.core/defn _0 {:placeholder true} [& args] (clojure.core/nth args 0)) 
    (clojure.core/defn _1 {:placeholder true} [& args] (clojure.core/nth args 1)) 
    (clojure.core/defn _2 {:placeholder true} [& args] (clojure.core/nth args 2)))

1 Very, very rarely


Edit

You can also do this completely without macros, by using a function to create functions and using the lower-level operation intern instead of def. It turns out to be much simpler, in fact:

(letfn [(placeholder [n]
          (fn [& args]
            (nth args n)))]
  (doseq [i (range 5)]
    (intern *ns* (symbol (str "_" i))
            (placeholder i))))
like image 172
amalloy Avatar answered Oct 28 '22 15:10

amalloy


Macroexpanding once the (make-placeholders 9) expression I get this:

(for
 [i__1862__auto__ (range 0 9)]
 (defn-from
     (str "_" i__1862__auto__)
     {:placeholder true}
   [& args]
   (nth args i__1862__auto__)))

So defn-from expects a string as first argument, but, because it is a macro, (str "_" i__1862__auto__) is not evaluated and is thus past in as a list.

I played around with this for a while and came up with this:

(defmacro make-placeholders [n]
  `(map eval
        '~(for [cntr (range 0 n)]
           `(defn ~(symbol (str "_" cntr))
               {:placeholder true} [& args] (nth args ~cntr)))))

Macroexpanding (make-placeholders 3) gives

(map eval
 '((defn _0 {:placeholder true} [& args] (nth args 0))
   (defn _1 {:placeholder true} [& args] (nth args 1))
   (defn _2 {:placeholder true} [& args] (nth args 2))))

which is what I intended and evaluating this defines the functions _0, _1 and _2:

;=> (_0 1 2 3)
1
;=> (_1 1 2 3)
2
;=> (_2 1 2 3)
3

Ok, so this works, but I am still not sure its a good idea to do this.

First off eval is evil. Ok, it could also be done without eval, but using do instead (replace map eval in my solution with do). But you are possibly making your code hard to understand because you create functions that are not defined anywhere in your code. I remember that when I just started using Clojure I was looking through some library for a function and I couldn't find it. I started to think yikes, this guy must have defined a macro somewhere that defines the function I am looking for, how am I ever going to understand what's going on? If this is how people are using Clojure then it is going to be one hell of a mess and dwarf everything people have said about Perl... It turned out I was just looking at the wrong version - but you may be setting yourself and others up for some hardship.

Having said this, there may be appropriate uses for this. Or you could use something similar to generate code and put that into some file (then the source code would be available for inspection). Maybe somebody more experienced can chime in?

like image 26
Paul Avatar answered Oct 28 '22 14:10

Paul