Janet 1.38.0-73334f3 Documentation
(Other Versions:
1.37.1
1.36.0
1.35.0
1.34.0
1.31.0
1.29.1
1.28.0
1.27.0
1.26.0
1.25.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.17.1
1.16.1
1.15.0
1.13.1
1.12.2
1.11.1
1.10.1
1.9.1
1.8.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.1
)
Macros
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 let
, loop
, and defn
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)
into
(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 (doc defn3)
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 ~
is
shorthand for the (quasiquote x)
special form, which is like (quote
x)
except we can unquote expressions inside it. The comma in front of
name
and 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
!
Similar to 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 (splice x)
special form for this purpose, and a shorthand for it, the ;
character.
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)))
Accidental Binding Capture
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 x
and 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
the (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))
After expansion, 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
hygiene
problem and is solved in many programming languages
(such as Scheme) by hygienic macros. Hygienic macros prevent such
accidental captures, but usually constrain the kinds of macros that can be written. Janet's macro system does not use hygienic macros as presented in Kohlbecker et al.'s 1986 paper - instead, programmer diligence is required to avoid accidental captures.
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.
(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 than a macro, as functions can be more easily passed around and used as first class values.