monojohnny
monojohnny

Reputation: 6181

Why doesn't work : "First argument to defn must be a symbol"

Why do I get the error:

IllegalArgumentException First argument to defn must be a symbol  clojure.core/defn (core.clj:277)

When I try to define a function like this:

(defn (symbol "f[]") 1)

Or like this:

(defn (symbol "f") [] 1)

Why aren't those the equivalent of straight forward example below ?

(defn f [] 1)

This is esoteric I know: but it just occurred to me that I might want to name a function dynamically at some point. (No real use case here - just trying to understand Clojure's mind...)

Upvotes: 1

Views: 811

Answers (4)

Alan Thompson
Alan Thompson

Reputation: 29958

You really don't need to use eval.

You have hit the problem known as "turtles all the way down". Once you try to treat a macro like a function (perhaps passing it to map, for example), you find you cannot do it without writing another macro. The same applies to macro #2, etc.

Thus, you can't compose macros as well as you can compose functions. This is the genesis of the general advice, "Never use a macro when you can use a function."

In this case, defn is a macro, so you have no choice but to write another macro (def behaves the same way, even though it is a special form instead of a macro). Our new macro dyn-defn dynamically creates the function name from a list of strings:

(defn fun-1 [] 1)
(def  fun-2 (fn [] 2))

; (def (symbol (str "fun" "-3")) (fn [] 3))
;   => Exception: First argument to def must be a Symbol

(defmacro dyn-defn
  "Construct a function named dynamically from the supplied strings"
  [name-strs & forms]
  (let [name-sym (symbol (str/join name-strs)) ]
    (spyx name-sym)
    `(defn ~name-sym ~@forms)))

(dyn-defn ["fun" "-3"]
  []
  3)

with result:

*************** Running tests ***************
:reloading (tst.demo.core)

name-sym => fun-3      ; NOTE:  this is evaluated at compile-time

Testing _bootstrap

-------------------------------------
   Clojure 1.9.0    Java 1.8.0_161
-------------------------------------

Testing demo.core

Testing tst.demo.core

(fun-1) => 1         ; NOTE:  these are all evaluated at run-time
(fun-2) => 2
(fun-3) => 3

Note that the function name is an argument to the defn macro, and must be a symbol, not a function call.


Note:

Correct, you can't tell by looking at it if a form is "calling" a function or a macro. In fact, many "build-in" features of Clojure are constructed from more fundamental parts of the language, whether macros like when (source code) or functions like into (source code).

Upvotes: 0

MasterMastic
MasterMastic

Reputation: 21286

The answer is what Josh said (defn is a macro; if it was a function then your code really would work in this way). You can define your own defn variation macro that would do what you want or just use eval:

(eval `(defn ~(symbol "f") [] 1))
; => #'user/f
(f)
; => 1

Upvotes: 2

Sylwester
Sylwester

Reputation: 48745

You are mixing code and data. It is a very common mistake to do. Eg.

(+ 4 5) ; ==> 9
('+ 4 5) ; ==> Error

'+ evaluates to a symbol. It is not the same as the variable + that is code and evaluates for a function. It's easy to check by evaluating them:

+  ; ==> #<core$_PLUS_ clojure.core$_PLUS_@312aa7c>
'+ ; ==> +

defn is a macro that expands to def so your beef is with def. The reason (def (symbol "x") 5) doesn't work is because def happens at compile time. The first arguments is never evaluated, but used for all references to the same identifiers within the same namespace. An expression like (symbol "x") won't work pretty much because of the same reason + and '+ cannot be mixed. You can do this in compile time though:

(defmacro make-fun [name expression]
  `(defn ~(symbol name) [] ~expression))

(macroexpand-1 '(make-fun "f" 1))
; ==> (clojure.core/defn f [] 1)

(make-fun "f" 1)
; ==> #'user/f

(f) ; ==> 1

So what is happening is that before the code runs (make-fun "f" 1) gets replaced with (clojure.core/defn f [] 1) and the runtime never ever sees where it came from. While this seems useful you still cannot use a binding or input to make your function:

(def fun-name "f")
(def fun-value 1)
(macroexpand-1 '(make-fun fun-name fun-value))
; ==> (clojure.core/defn fun-name [] fun-value)

Macros are just a way to simplify and abstract on syntax. If you always write a pattern that looks like (defn name [& args] (let ...) you can make the parts that differ bindings in a macro and shorten every place you use the abstraction with the new macro. It is a code translation service. In compile time the arguments are just the literal code that it is suppsoed to replace and you never have the luxury to see if a variable or expression has a certain value since you only knows about the code and never what they actually represent. Thus the errors usually arises in when the code in the end result runs.

In the end you can do anything in runtime with eval. I've seen eval being used in a sensible manner twice in my 19 year run as a professional programmer. You could do:

(defn make-fun [name value]
  (eval `(defn ~(symbol name) [] ~value)))

(make-fun fun-name fun-value)
; #'user/f
(f)
; ==> 1

Now while this works you shouldn't do it unless this is some sort of tool to test or do something with code rather than it being a part of the code to be run as a service with the string coming in from a unsafe source. I would have opted for using dictionaries instead such that you do not update your own environment. Imagine if the input was make-fun or some other part of your code that would give the client control over your software.

Upvotes: 2

Josh
Josh

Reputation: 4806

When you pass arguments to a macro, they are not evaluated beforehand. Since defn is a macro, what you're passing it in those two cases are not equivalent.

Upvotes: 4

Related Questions