Common Cold's special forms
The central element in serialising continuations are serialisable
closures. They are created by the macros slambda (serialisable lambda)
and sfunction (serialisable function), which can be used exactly like
the lambda and function special forms, except that slambda may not be
the car of an expression (or inside a function form), and that
sfunction can only be given a function's name as argument. These macros
will globally register the information needed to serialise and
deserialise the closures at compile-time, in a serialisable format.
For example, a serialisable adder could be created this way
(defun make-adder (inc)
(flet ((adder (x)
(+ x inc)))
(sfunction adder)))
or that way
(defun make-adder (inc)
(slambda (x)
(+ x inc)))
Note that there is no need to annotate local variables or
functions. Highly unportable environment access is used to regenerate
information about the lexical scope surrounding an slambda or sfunction
form. Unfortunately, it means that it may also interact badly with
codewalked extensions.
Something close to Administrative-Normal Form (ANF, or monadic style)
is then used to expose enough structure to macros for them to be able
to easily save continuations. (bind (var val) expr) binds the
value of val to var in expr, adding expr to val's
continuation. dbind is the same thing, but binds to special
(dynamically scoped) variables instead of lexical ones. This is enough
to make explicit the order in which operations will be executed, which
is exactly what is needed to implement continuations. There is no need
for a monadic return: we use dynamically scoped constructs (catch and
throw), so values need not be wrapped specially.
ANF is much less painful to use than CPS, but it's still far from
ideal. Common Cold offers several macros to emulate common CL values
binding forms. Instead of let*, one should use mlet* (to bind to
lexical variables) or mdlet* (to bind to special variables). For
example:
(mlet* ((x (get-x))
(y (get-y))
(dist (sqrt (+ (* x x)
(* y y)))))
(fn dist))
or, if we want fn to have access to x and y as special variables:
(mdlet* ((*x* (get-x))
(*y* (get-y)))
(fn (sqrt (+ (* *x* *x*)
(* *y* *y*)))))
Note that normal CL lexical binding forms (e.g., let or let*) may be
freely mixed with CC's forms, as long as the execution of the value
expressions ((get-x), (get-y), ... here) does not capture
continuations. CL dynamic binding forms (e.g. with (declare (special...))),
however, should only be used when the execution of both the value
expressions and the body of the forms (the dynamic extent of the
bindings) does not capture continuations. mlet (for lexical bindings)
and mdlet (for dynamic ones) offer a behaviour like that of CL's let,
where the bindings are only established once all the bound values have
been computed.
Creating serialisable closures and continuations establishes copies of
the lexical bindings. Assignment to lexical variables is thus not
recommended, at least not when slambda or sfunction forms are in the
lexical scope, or continuation-capturing code in the dynamic
scope. Dynamic bindings, on the other hand, are linked to the dynamic
environment, and not to continuation frames or closures. Thus, it makes
sense to capture them with the rest of the dynamic environment — at
least, as much of it — in continuations. Capturing continuations will
also save (copy) the values of all the bindings at the time of the
capture, so assignment to special variables is meaningful... without
continuations making it harder to reason about them.
Common Lisp's non-local exit forms (e.g., throw or tagbody) all have a
dynamic extent, which means they are useless after the capture and
later invocation of a continuation. Common Cold duplicates some of
these forms in order to be able to restore them when invoking
continuations. mcatch may be used exactly like catch, returning to it
with a normal throw. mblock (which, like block, establishes a lexical
non-local exit environment), on the other hand, must be matched with
mreturn-from or mreturn (which act like return-from and
return). Returning to a CL block with mreturn-from, or to an mblock
with return-from will result in erroneous code. Fortunately, they are
lexically scoped constructs, so using them correctly should be easy
(checkable locally).
Finally, to execute several operations in sequence, one should use
mprogn, which acts like progn. However, most of the special forms
above have implicit mprogn (when their CL counterparts have implicit
progn).
posted at: 00:00 | /Lisp/CommonCold | permalink
