Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I programmatically generate record definitions?

In my answer to a Code Review.SE question, I suggested that the OP might consider using records to represent chess pieces. Since the piece records would all be the same, except for the name, I figured I could generate them programmatically, like this:

(map #(defrecord % [color]) 
      ["Rook" "Pawn" "Queen" "King" "Knight" "Bishop"])

That sort of worked, but my record names weren't the piece names; they were random gensyms: instead of user.Rook I got user.p1__910. If I did (p1__910. :black), it did work and create a record, but you can probably see why I wasn't satisfied with that.

I also tried the following two variations:

(map #(defrecord % [color]) 
      ['Rook 'Pawn 'Queen 'King 'Knight 'Bishop])
  ;; Same result as above.
(map #(defrecord (symbol %) [color])
           ["Rook" "Knight" "Pawn" "Queen" "King" "Bishop"])
  ;; CompilerException java.lang.ClassCastException: clojure.lang.PersistentList 
  ;; cannot be cast to clojure.lang.Symbol, compiling:(NO_SOURCE_PATH:1:7) 

What's wrong with my approach? How can I generate a bunch of identical records from a list of names? Is this even possible?

like image 697
tsleyson Avatar asked Dec 24 '22 20:12

tsleyson


2 Answers

This is a classic case of macro-contagion.

user> defrecord
CompilerException java.lang.RuntimeException: Can't take value of a macro: #'clojure.core/defrecord, compiling:(/tmp/form-init802461651064926183.clj:1:5990) 

You where very close with the (symbol %) idea you just needed to make it so the defrecord expressions generated are evaluated after you provide the values.

user> (defmacro make-pieces [piece-names]
        `(do ~@(map #(list 'defrecord (symbol %) '[color]) 
                    piece-names)))
#'user/make-pieces

user> (macroexpand-1 '(make-pieces ["Rook" "Pawn" "Queen" "King" "Knight" "Bishop"]))
(do (defrecord Rook [color]) 
    (defrecord Pawn [color]) 
    (defrecord Queen [color]) 
    (defrecord King [color]) 
    (defrecord Knight [color]) 
    (defrecord Bishop [color]))

user> (make-pieces ["Rook" "Pawn" "Queen" "King" "Knight" "Bishop"])
user.Bishop
like image 167
Arthur Ulfeldt Avatar answered Feb 15 '23 13:02

Arthur Ulfeldt


If all the records are the same, why give them different names? I'd suggest:

(defrecord Chess-Piece [name color])

What's wrong with your approach is that defrecord is a macro, so the "name" argument is interpreted as a symbol and so determines the name for the record before compiling. The mapping only occurs during runtime, after compilation.

The % in your anonymous function is rewritten as a gensym (p1__910), which in turn is interpreted as the symbol naming your new record.

What you want to do would have to be done with a macro -- you must simply ensure that by the time (defrecord some-symbol [color]) is evaluated (again, this is pre-runtime), some-symbol is what you want it to be. Maybe something along the lines of:

(defmacro defpieces [names]
  (let [defs (map #(list 'defrecord (symbol %) '[color])
                  names)]
    `(do ~@defs)))

How your code is being rewritten:

(map #(defrecord % [color]) 
  ["Rook" "Pawn" "Queen" "King" "Knight" "Bishop"])

With reader macros, this turns into (roughly):

(map (fn* [p1__910#] (defrecord p1__910# [color])
  ["Rook" "Pawn" "Queen" "King" "Knight" "Bishop"])

defrecord is itself a macro, so (again, before runtime) this is transformed into a giant block of code that contains:

(deftype* p1__910# user.p1__910# .....

To see the whole block, use the very useful macroexpand:

(macroexpand '(defrecord p1__910# [color]))
like image 34
galdre Avatar answered Feb 15 '23 14:02

galdre