Hi, Emacs! -- mode: gfm --
This project is an attempt at adding gensym
functionality to LFE, giving
hackers a mechanic to help avoid the accidental capture of identifiers when
writing macros.
If LFE is your first exposure to Lisp, you might not have come across gensym
before, so I will attempt to describe it here and show an example of its usage.
First, let us consider the following trivial macro:
(defmacro square (value)
`(* ,value ,value))
At first glance, the macro is simple enough; it generates code that multiplies
value
by itself. This is a highly trivialised example that you will probably
never see in the real world.
Now, let us imagine that square
is invoked with a function as its
argument. Imagine that the function is:
(defun random (limit)
(erlang:trunc (* (rand:uniform) limit)))
What happens when we invoke (square (random 10))
? Place bets now.
Let's walk through an exansion:
(square (random 10))
⇒ (* (random 10) (random 10))
⇒ (* 3 (random 10))
⇒ (* 3 8)
⇒ 24
Unless you already knew about this issue, this result is probably not what you were expecting.
This is where gensym
comes into play. The gensym
function itself returns a
fresh symbol that can be used inside a macro body in order to provide a means of
temporarily storing a value.
We could re-write square
in this way:
(defmacro square (value)
(let ((temp (gensym)))
`(let ((,temp ,value))
(* ,temp ,temp))))
Now when we invoke (square (random 10))
, the following expansion occurs:
(square (random 10))
⇒ (let ((sym_1 (random 10)))
(* sym_1 sym_1))
⇒ (let ((sym_1 8))
(* sym_1 sym_1)
⇒ (let ((sym_1 8))
(* 8 sym_1)
⇒ (let ((sym_1 8))
(* 8 8)
⇒ 64
As you can see, this technique allows us to ensure that evaluation only occurs once.
This might look like a lot of typing, but imagine we could do something like:
(defun square (x)
(with-gensyms (temp)
`(let ((,temp ,x))
(* ,temp ,temp))))
Turns out that we can. This package provides an with-gensyms
macro in
include/lfe_gensym.lfe
.
What if we want to write the square
macro like this:
(defun square (x)
(once-only (x)
`(* ,x ,x)))
Turns out that we can write the macro like this, too. This package also
provides a once-only
macro in include/lfe_gensym.lfe
.
In an attempt to avoid causing Erlang to hard crash due to exhaustion of the
atom table, this gensym implementation defaults to allowing only 10% of the atom
table to be used for gensym's purposes. Once this limit is reached, gensym
will return a symbol that already exists. This should not cause any issues,
however.
With that said, this has not been tested with distributed code, so caveat emptor.
Unlike other Lisps where gensym
takes an argument and/or variables like
*gensym-counter*
can be modified to influence the name and/or numeric
component of the next symbol generated, this implementation takes zero
arguments. This is to help prevent atom table exhaustion, and should not have
any detrimental affect on code, but it is something to watch out for when
porting code from other Lisp implementations.
gensym
⇒ new-symbol
- new-symbol — a fresh symbol.
Creates and returns a new symbol. The symbol's prefix is of the form
sym_; e.g. sym_1
. The number is incremented each time.
The value of number is that of gensym_counter
.
(gensym) ⇒ sym_24
The resulting symbol is really an Erlang atom, and is interned in Erlang's atom table.
Will increment gensym_counter
.
Should the gensym counter reach the limit (as returned by gensym_limit
), the
counter will wrap around to 1.
gensym_counter
Notes:
- Unlike Common Lisp or Maclisp, it is not possible to modify the name of the generated symbol. This is to try to avoid allowing one to shoot oneself in the foot by creating too many Erlang atoms.
gensym_counter
⇒ number
- number — a non-negative integer.
Returns a numeric value that will be used in constructing the name of the next
symbol generated by gensym
.
(gensym_counter) ⇒ 18
(gensym) ⇒ sym_18
(gensym_counter) ⇒ 19
gensym
- Unlike Common Lisp, this is a function and not a variable.
gensym_limit
⇒ number
- number — a non-negative integer.
Returns the maximum amount of symbols that may be created by gensym
.
(gensym_limit) ⇒ 104857
- The gensym limit shall be considered to be a value that is approximate to 10%
of the Erlang atom table size. This can be changed with the
-t
CLI argument.
once-only
(
symbol_1 symbol_n ... )
form ... )
⇒ result
- symbol — a symbol.
- form — a form.
- result — a form.
Execute form in a context where the values bound to the symbols in symbol_1 through n are bound to fresh gensyms, and the symbols in symbol_1 through n are bound to the corresponding gensym.
This macro ensures that symbols passed as variables are only executed once within the context of the body form.
Taken from Peter Norvig's Paradigms of Artificial Intelligence Programming.
(defun random (limit)
(erlang:rem
(erlang:trunc (* (rand:uniform) limit))
limit))
⇒ random
(defmacro square (x)
(once-only (x)
`(* ,x ,x)))
⇒ square
(macroexpand-all '(square (random 10)) $ENV)
⇒ (let ((sym_1 (random 10)))
(call 'erlang '* sym_1 sym_1))
(square (random 10))
⇒ 64
system_info
⇒ (tuple
limit count )
- limit — a non-negative integer.
- count — a non-negative integer.
Returns information about the gensym system.
Limit is the maximum number of symbols that can be generated with gensym
.
Count is the total number of symbols that have been created with gensym
.
(lfe_gensym:system_info) ⇒ #(104857 0)
(lfe_gensym:gensym) ⇒ sym_1
(lfe_gensym:system_info) ⇒ #(104857 1)
gensym
with-gensym
(
symbol_1 symbol_n ... )
form ... )
⇒ result
- variable — a symbol.
- form — a form.
- result — a form.
Execute form in a context where the values bound to the symbols in symbol_1 through n are bound to fresh gensyms
Taken from Paul Graham's On Lisp.
(defmacro nif (val pos zero neg)
(with-gensyms (thing)
`(let ((,thing ,val))
(cond ((> ,thing 0) ,pos)
((== ,thing 0) ,zero)
(else ,neg)))))
⇒ nif
(macroexpand-all '(nif 42 'positive 'zero 'negative) $ENV)
⇒ (let ((sym_14 42))
(if (call 'erlang '> sym_14 0)
(progn 'positive)
(if (call 'erlang '== sym_14 0)
(progn 'zero)
(progn 'negative))))
(nif 42
(io:format "Positive~n" '())
(io:format "Zero~n" '())
(io:format "Negative~n" '()))
⇒ Positive