unwind-protect
(2015 February 13) ¶NOTE On the implementation of
unwind-protect
(part 2) (2015 February 19 bis) for part 2.
The macro unwind-protect
has the purpose of releasing resources that must be
allocated synchronously with respect to a chunk of code; its syntax is:
(unwind-protect ?body ?clean-up0 ?clean-up ...)
and it can be used as:
#!vicare (let ((port (open-file-output-port "file.ext"))) (unwind-protect (put-bytevector port '#ve(ascii "ciao")) (close-output-port port)))
first we allocate a resource (in this case the port); then we use it in the
?body form; finally we release it in the ?clean-up forms. The
?clean-up forms are executed whether ?body performs a normal return or
raises an exception. When ?body returns: the return value of
unwind-protect
is the return value of ?body.
It is clear that unwind-protect
is a very useful operator: in a way or the
other, a language must provide a way to release synchronous resources. We
might be tempted to rely on the garbage collector finalisers (under Vicare:
guardians), but:
There have been
plenty of
discussions about how to implement unwind-protect
in Scheme and some
proposals; none of them
provided a definitive solution (where definitive means: problem solved and
syntax included in the standard language). At
present, Vicare’s master
branch implements an unwind–protection mechanism
that, at its core, is available through the built–in macro
with-unwind-protection
exported by the library (vicare)
; the syntax
is:
(with-unwind-protection ?clean-up ?thunk)
where ?thunk performs the job and the procedure ?clean-up releases resources.
The above example would be implemented as:
#!vicare (let ((port (open-file-output-port "file.ext"))) (with-unwind-protection (lambda (why) (close-output-port port)) (lambda () (put-bytevector port '#ve(ascii "ciao")))))
The built–in macro unwind-protect
is still available as simple wrapper
around with-undind-protection
:
(unwind-protect ?body ?cleanup0 ?clean-up ...) → (with-unwind-protection (lambda (dummy) ?cleanup0 ?clean-up ...) (lambda () ?body))
the built–in macros try
and with-compensations
are also
implemented on top of with-unwind-protection
.
So, how does it work? The idea is to define the concept of dynamic extent termination of a call to function; this concept is defined by Vicare’s unwind–protection mechanism and it is not a r6rs concept. To understand the unwind–protection mechanism we must understand the concepts “dynamic extent of a function call” and “dynamic environment”.
The procedure ?clean-up is called when the dynamic extent of the invocation of
?thunk terminates; dynamic extent termination is different from
dynamic extent exiting as determined by dynamic-wind
. When the
execution flow exits the dynamic extent of a function call: such extent might also
terminate, but not all the exits are also terminations.
In this discussion, we consider the syntax use:
(with-unwind-protection ?clean-up ?thunk)
the dynamic extent of a call to ?thunk is terminated, and so ?clean-up is invoked, when:
guard
, but only when a clause of guard
has a test
expression returning non–false. If we do:
(guard (E (?test ?expr)) (with-unwind-protection ?clean-up (lambda () (error #f "I know what you did"))))
this is what happens:
guard
and it returns non–false.
with-unwind-protection
and its return values discarded. ?clean-up is
applied to the symbol ‘exception’.
guard
and its return values are returned to the continuation of
guard
.
break
, continue
or return
, as defined by the library (vicare)
, to escape from a form
that encloses an unwind–protection syntax. ?clean-up is applied to the symbol
‘escape’.
As bound by the loop syntaxes while
, until
, … and the syntax
returnable
: these fluid syntaxes reinstate a continuation at the beginning
or outside of while
, until
, … and returnable
and
perform special operations to terminate the dynamic extent of the call to
?thunk in an unwind–protection form.
The dynamic extent of a call to ?thunk is not terminated, and so ?clean-up is not invoked, when:
raise-continuable
, and
such call performs a normal return to ?thunk.
exit
).
yield
is called from
within ?thunk to hand control to another coroutine.
About the termination of the dynamic extent of ?thunk, we must acknowledge that:
&non-reinstatable
component.
guard
intercepts it: if a test expression in a clause of guard
raises a second
exception, the ?clean-up may not be called.
dynamic-wind
raise a second exception: the clean–up operation may
fail (and, for example, leave the resource unreleased and in an incorrect state).
Things to notice:
raise
any object: it is better to always raise a
condition object (possibly compound), so that the test expressions in guard
uses can just be condition object type predicates; such predicates never raise
exceptions.
The problem of the second exception does not haunt only Vicare; if I understand
correctly, unwind-protect
as
defined by Common Lisp also presents cases where the clean–up forms may not be
called.
The unwind–protection mechanism has the purpose of evaluating code when the dynamic
extent of a function call terminates; it can be used to release synchronous
resources: resources whose life must terminate when the dynamic extent of a function
call terminates. Whenever possible, we must take care of handling exceptions with
guard
before they cross the unwind–protection boundary. So, with
compensations, we might do:
(with-compensations (guard (E (?test ?expr) ...) (define ?resource-reference (compensate ?alloc (with ?release))) ... ?body0 ?body ...))
or, if we like try
:
(with-compensations (try (letrec ((?resource-reference (compensate ?alloc (with ?release))) ...) ?body0 ?body ...) (catch E ((?condition-type) ?expr) ...)))