Janet supports macros: routines that take code as input and return transformed code as output. A macro is like a function, but transforms the code itself rather than data, so it is more flexible in what it can do than a function. Macros let you extend the syntax of the language itself.
You have seen some macros already. The
forms are macros. When the compiler sees a macro, it evaluates the macro and
then compiles the result. We say the macro has been expanded after the
compiler evaluates it. A simple version of the
defn macro can be thought
of as transforming code of the form
(defn1 myfun [x] body)
(def myfun (fn myfun [x] body))
We could write such a macro like so:
(defmacro defn1 [name args body] (tuple 'def name (tuple 'fn name args body)))
There are a couple of issues with this macro, but it will work for simple functions quite well.
The first issue is that our
defn1 macro can't define functions with
multiple expressions in the body. We can make the macro variadic, just like a
function. Here is a second version of this macro:
(defmacro defn2 [name args & body] (tuple 'def name (apply tuple 'fn name args body)))
Great! Now we can define functions with multiple elements in the body.
We can still improve this macro even more though. First, we can add a docstring
to it. If someone is using the function later, they can use
to get a description of the function. Next, we can rewrite the macro using
Janet's builtin quasiquoting facilities.
(defmacro defn3 "Defines a new function." [name args & body] ~(def ,name (fn ,name ,args ,;body)))
This is functionally identical to our previous version
defn2, but written
in such a way that the macro output is more clear. The leading tilde
shorthand for the
(quasiquote x) special form, which is like
x) except we can unquote expressions inside it. The comma in front of
args is an unquote, which allows us to put a value in the
quasiquote. Without the unquote, the symbol
name would be put in the
returned tuple, and every function we defined would be called
name, we must also unquote
body. However, a normal
unquote doesn't work. See what happens if we use a normal unquote for
body as well.
(def name 'myfunction) (def args '[x y z]) (defn body '[(print x) (print y) (print z)]) ~(def ,name (fn ,name ,args ,body)) # -> (def myfunction (fn myfunction (x y z) ((print x) (print y) (print z))))
There is an extra set of parentheses around the body of our function! We don't
want to put the body inside the form
(fn args ...), we want to
splice it into the form. Luckily, Janet has the
special form for this purpose, and a shorthand for it, the
When combined with the unquote special, we get the desired output:
~(def ,name (fn ,name ,args ,;body)) # -> (def myfunction (fn myfunction (x y z) (print x) (print y) (print z)))
Sometimes when we write macros, we must generate symbols for local bindings. Ignoring that this could be written as a function, consider the following macro:
(defmacro max1 "Get the max of two values." [x y] ~(if (> ,x ,y) ,x ,y))
This almost works, but will evaluate both
y twice. This is
because both show up in the macro twice. For example,
(max1 (do (print 1)
1) (do (print 2) 2)) will print both 1 and 2 twice, which would be surprising
to a user of this macro.
We can do better:
(defmacro max2 "Get the max of two values." [x y] ~(let [x ,x y ,y] (if (> x y) x y)))
Now we have no double evaluation problem! But we now have an even more subtle problem. What happens in the following code?
(def x 10) (max2 8 (+ x 4))
We want the maximum to be 14, but this will actually evaluate to 12! This can be
understood if we expand the macro. You can expand a macro once in Janet using
(macex1 x) function. (To expand macros until there are no macros
left to expand, use
(macex x). Be careful: Janet has many macros, so the
full expansion may be almost unreadable).
(macex1 '(max2 8 (+ x 4))) # -> (let (x 8 y (+ x 4)) (if (> x y) x y))
y wrongly refers to the
x inside the macro (which
is bound to 8) rather than the
x defined to be 10. The problem is the
reuse of the symbol
x inside the macro, which overshadowed the original
binding. This problem is called the
problem and is well known in many programming languages. Some languages provide
complicated solutions to this problem, but Janet opts for a much
simpler—if not primitive—solution.
Janet provides a general solution to this problem in terms of the
(gensym) function, which returns a symbol which is guaranteed to be
unique and not collide with any symbols defined previously. We can define our
macro once more for a fully correct macro.
janet (defmacro max3 "Get the max of two values." [x y] (def $x (gensym)) (def $y (gensym)) ~(let [,$x ,x ,$y ,y] (if (> ,$x ,$y) ,$x ,$y)))
Since it is quite common to create several gensyms for use inside a macro body,
Janet provides a macro
with-syms to make this definition a bit terser.
(defmacro max4 "Get the max of two values." [x y] (with-syms [$x $y] ~(let [,$x ,x ,$y ,y] (if (> ,$x ,$y) ,$x ,$y))))
As you can see, macros are very powerful but are also prone to subtle bugs. You must remember that at their core, macros are just functions that output code, and the code that they return must work in many contexts! Many times a function will suffice and be more useful that a macro, as functions can be more easily passed around and used as first class values.