2
votes

I'm trying to write my first ever macro in Clojure. I want to mimic Ruby's %w{} operator, which works like this:

irb(main):001:0> %w{one two three}
=> ["one", "two", "three"]

I want to write a function similar in Clojure that returns a vector of words. Here is how it would look:

user=> (%w one two three)
=> ["one" "two" "three"]

I know this is something that cannot be defined as an ordinary function because the symbols would be evaluated before applying and we would see something like this:

user=> (%w one two three)
CompilerException java.lang.RuntimeException: Unable to resolve symbol: one in this context, compiling:(NO_SOURCE_PATH:1:1)

Here is my attempt at a macro:

(defmacro %w [& words]
  (map str (vec words)))

But it doesn't work.

user=> (%w one two three)
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn  user/eval801 (NO_SOURCE_FILE:1)

Why is this happening?

ANSWERS

So the problem was that the macro actually returned the correct output, but then the repl tried to evaluate it and "one" is not a valid function.

Thanks to the answers below, here are two correct macros that solve this problem:

(defmacro %w-vec [& words]
  "returns a vector of word strings"
  (mapv str (vec words)))

(defmacro %w-list [& words]
  "returns a list of word strings"
  (cons 'list (map str words)))
2

2 Answers

3
votes

It does not work because after macro-expansion clojure tries to evaluate ("one" "two" "three"), inducing your error message

user=> (%w one two three)
ClassCastException java.lang.String ("one") cannot be cast to clojure.lang.IFn (interface for callable stuff)  user/eval801 (NO_SOURCE_FILE:1)

now you could do that

(defmacro %w [& words]
  (mapv str (vec words)))

generating a vector

or that

(defmacro %w [& words]
  (cons 'list (mapv str (vec words))))

generating (list "one" "two" "three")

or with syntax quote

(defmacro %w [& words]
  `(list ~@(map str (vec words))))
2
votes

Macros can be thought of as regular functions that take unevaluated code (as data structures) and return new code (as data), which then gets evaluated.

Your macro is returning ("one" "two" "three"), which will evaluate as a call of the function "one" with arguments "two" "three".

Straightforward solutions would be to make your macro return either (list "one" "two" "three") or a vector ["one" "two" "three"].