unwind-protect
(part 2) (2015 February 19 bis) ¶In a previous entry (see On the implementation of unwind-protect
(2015 February 13)) I discussed the mechanism of unwind
protection as implemented by Vicare, without showing many examples. This is to
be remedied here. This section is about what’s currently in the master branch,
not the latest Vicare release.
CONFESSION Some of this weblog’s entries are written by cheating: big portions of text and code are small reorganisations of Vicare’s documentation and test suite programs.
When using the mechanism:
dynamic-wind
calls. These would be nasty
errors, because the dynamic environment would get corrupted and recovering correctly
might be impossible; so we must really write and debug such in–guard and
out–guard thunks with care.
(with-exception-handler (lambda (E) (with-blocked-exceptions (lambda () (handle E)))) (lambda () (do-something-useful)))
but it is not very satisfying.
If the bodacious answer to both the decisions is “We trust”: we can nest at will
uses of the with-unwind-protection
syntax:
(define (outer) (with-unwind-protection (lambda (why) (outer-unwind-handler)) (lambda () (do-something-useful) (inner)))) (define (inner) (with-unwind-protection (lambda (why) (inner-unwind-handler)) (lambda () (do-some-other-useful-thing)))) (outer)
beware: unwind handlers are evaluated in the dynamic environment of the use
of with-unwind-protection
.
Otherwise we must avoid error handlers to cause the execution flow to cross an unwind
protection boundary; we can do it by using the standard guard
syntax:
(define (outer) (push-compensation-thunk (lambda () (outer-unwind-handler))) (do-something-useful) (inner)) (define (inner) (push-compensation-thunk (lambda () (inner-unwind-handler))) (do-some-other-useful-thing)) (with-compensations (with-blocked-exceptions (lambda () (guard (E ((type-one-error? E) (handle E)) (else (do-something-dammit!!!))) (outer)))))
or by using the try
syntax defined by Vicare:
(define (outer) (push-compensation-thunk (lambda () (outer-unwind-handler))) (do-something-useful) (inner)) (define (inner) (push-compensation-thunk (lambda () (inner-unwind-handler))) (do-some-other-useful-thing)) (with-compensations (with-blocked-exceptions (lambda () (try (outer) (catch E ((&type-one) (handle E)) (else (do-something-dammit!!!)))))))
beware: compensation handlers are evaluated in the dynamic environment of
the use of with-compensations
4.
To follow how the unwind protection mechanism intertwines itself with the protected
code, we need a tracking mechanism. In this section we use the with-result
api exported by the library (vicare checks)
; it works like this:
(import (vicare) (only (vicare checks) with-result add-result)) (with-result (add-result 1) (add-result 2) 3) ⇒ (3 (1 2)) (with-result (add-result 1) (add-result 2) (values 3 4)) ⇒ (3 4 (1 2))
we wrap the forms to track in a use of with-result
, then, inside the form,
we call add-result
at “interesting points”; with-result
returns a
list whose head items represent the returned values and whose last item represents
the FIFO queue of values to which add-result
was applied. Very
rough, but simple.
In all the sample code we have to assume the following prelude:
(import (vicare) (only (vicare checks) with-result add-result))
and we refer to ?thunk and ?unwind-handler as with the following roles:
(with-unwind-protection ?unwind-handler ?thunk) (with-exception-handler ?exception-handler ?thunk)
Let’s get on with the examples.
When no exceptions are raised and no escape procedures are called, the evaluation just goes; the ?unwind-handler is evaluated after ?thunk.
(with-result (with-unwind-protection (lambda (why) (add-result 'out)) (lambda () (add-result 'in) 1))) ⇒ (1 (in out))
If we call an escape procedure from ?thunk, this is what happens:
(with-result (call/cc (lambda (escape) (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (add-result 'body) (escape 1)))))) ⇒ (1 (body))
we see that the unwind protection handler is not called. This is correct! We escaped from the dynamic extent of the call to ?thunk by calling a raw escape procedure; not calling the unwind protection handler in this situation allows us to implement coroutines on top of continuations. Obviously, there is the downside that a raw escape might skip releasing resources; there is nothing we can do about it.
Continuable exceptions are raised with the standard raise
function. The only
way to react to a raised non–continuable exception is to intercept it with an
exception handler and, from its body, to call an escape procedure to exit the dynamic
environment that caused the exception:
(call/cc (lambda (escape) (with-exception-handler (lambda (E) (escape E)) (lambda () (raise 1))))) ⇒ 1 (call/cc (lambda (escape) (with-exception-handler escape (lambda () (raise 1))))) ⇒ 1
by tracking the evaluation flow we can verify that (as mandated by r6rs): raising an exception, by itself, does not cause an exit from the dynamic extent of the call to ?thunk, it is calling the escape procedure that causes the exit:
(with-result (call/cc (lambda (escape) (with-exception-handler (lambda (E) (add-result 'exception-handler) (escape E)) (lambda () (dynamic-wind (lambda () (add-result 'in-guard)) (lambda () (add-result 'body) (raise 1)) (lambda () (add-result 'out-guard)))))))) ⇒ (1 (in-guard body exception-handler out-guard))
saving a continuation and calling the escape procedure from the exception handler is
what the standard guard
syntax does; but guard
does more than this.
So let’s add an unwind protection handler:
(with-result (call/cc (lambda (escape) (with-exception-handler (lambda (E) (add-result 'exception-handler) (escape E)) (lambda () (dynamic-wind (lambda () (add-result 'in-guard)) (lambda () (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (add-result 'body) (raise 1)))) (lambda () (add-result 'out-guard)))))))) ⇒ (1 (in-guard body exception-handler out-guard))
we see that ?unwind-handler is not called; this is the same as the
previous example of escaping from ?thunk. How do we cause a call to the unwind
handler? By using the standard guard
syntax, which is specially interfaced
with the unwind protection mechanism:
(with-result (guard (E (else (add-result 'guard-else) E)) (dynamic-wind (lambda () (add-result 'in-guard)) (lambda () (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (add-result 'body) (raise 1)))) (lambda () (add-result 'out-guard))))) ⇒ (1 (in-guard body out-guard in-guard unwind-handler out-guard guard-else))
this is what happens (brace yourself):
guard
is applied to the raised object.
guard
that the else
clause must be evaluated.
else
clause is evaluated.
The following example has guard
evaluate a non–else
clause, which
makes the mechanism even more clear:
(with-result (guard (E ((begin (add-result 'guard-test-1) (string? E)) (add-result 'guard-expr-1) E) ((begin (add-result 'guard-test-2) (fixnum? E)) (add-result 'guard-expr-2) E) (else (add-result 'guard-else) E)) (dynamic-wind (lambda () (add-result 'in-guard)) (lambda () (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (add-result 'body) (raise 1)))) (lambda () (add-result 'out-guard))))) ⇒ (1 (in-guard body out-guard guard-test-1 guard-test-2 in-guard unwind-handler out-guard guard-expr-2))
it is the non–false result from evaluating a guard
test expression that
causes the termination of the dynamic extent of the call to ?thunk.
Everything we discussed for non–continuable exceptions applies unchanged to
continuable exceptions when we do not actually continue. A continuable exception is
raised with raise-continuable
and it is a way to resume the execution of a
chunk of code after raising an exception. This example shows the basic mechanism:
(with-exception-handler (lambda (E) (+ E 2)) (lambda () (raise-continuable 1))) ⇒ 3
if the exception handler returns to the caller, rather than calling an escape
procedure: raise-continuable
returns, too.
If we continue after raising a continuable exception, the evaluation flow is not different from a normal evaluation:
(with-result (with-exception-handler (lambda (E) (add-result 'exception-handler) (+ E 2)) (lambda () (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (add-result 'thunk-in) (begin0 (raise-continuable 1) (add-result 'thunk-out))))))) ⇒ (3 (thunk-in exception-handler thunk-out unwind-handler))
We can see a significant difference when we: raise a continuable exception in
?thunk; go through a guard
with no else
, which re–raises the
continuable exception; execute an exception handler; return to ?thunk; perform
normal a return:
(with-result (with-exception-handler (lambda (E) (add-result 'exception-handler) (+ 2 E)) (lambda () (guard (E ((begin (add-result 'guard-test) (error? E)) (add-result 'guard-error) E)) (with-unwind-protection (lambda (why) (add-result 'unwind-handler)) (lambda () (dynamic-wind (lambda () (add-result 'in-guard)) (lambda () (add-result 'thunk-in) (begin0 (raise-continuable 1) (add-result 'thunk-out))) (lambda () (add-result 'out-guard))))))))) ⇒ (3 (in-guard thunk-in out-guard guard-test in-guard exception-handler thunk-out out-guard unwind-handler))
when a guard
with no else
clause is used and all the
guard
’s test expressions evaluate to #f
: the exception is re–raised
with raise-continue
. ?unwind-handler is called only after the
evaluation flow has re–entered ?thunk and performed a normal return.
The unwind protection mechanism has special integration with the loop syntaxes
defined by (vicare)
; if an unwind protection syntax is used in the body of a
loop as defined by do
, while
, until
, for
and
break
or continue
are used in the body forms: the unwind handlers
are evaluated correctly.
We have to remember that break
and continue
are implemented by
escaping continuations, so they have to be specially interfaced with unwind
protection, otherwise the ?unwind-handler is not called.
This example shows breaking out of a while
syntax; the body and the unwind
handler are evaluated only once; exit
is never called.
(define x 3) (define y #f) (while (positive? x) (with-unwind-protection (lambda (why) (set! y #t)) (lambda () (-- x) (break) (exit)))) x ⇒ 2 y ⇒ #t
This example shows using continue
in a while
syntax; the body and
the unwind handler are evaluated 3 times; exit
is never called.
(define x 3) (define y 0) (while (positive? x) (with-unwind-protection (lambda (why) (++ y)) (lambda () (-- x) (continue) (exit)))) x ⇒ 0 y ⇒ 3
The unwind protection mechanism has special integration with the returnable
syntax defined by (vicare)
; if an unwind protection syntax is used in the
body of returnable
and return
is used in the ?thunk:
?unwind-handler is evaluated correctly. We have to remember that
return
is implemented by an escaping continuation.
(define y #f) (returnable (with-unwind-protection (lambda (why) (set! y #t)) (lambda () (return 1)))) ⇒ 1 y ⇒ #t
Using the unwind protection mechanism with coroutines is fine; coroutines use
yield
to save the current continuation, give control to the next coroutine and
come back later; this mechanism does not cause the ?unwind-handler evaluation.
The following sample code finishes with the given return values and lines printed:
(define (print template . args) (apply fprintf (current-error-port) template args) (yield)) (define a #f) (define b #f) (define c #f) (concurrently (lambda () (unwind-protect (begin (set! a 1.1) (print "unwind-protect sub 1.1: ~a\n" a) (set! a 1.2) (print "unwind-protect sub 1.2: ~a\n" a) (set! a 1.3) (print "unwind-protect sub 1.3: ~a\n" a)) (set! a 1.4))) (lambda () (unwind-protect (begin (set! b 2.1) (print "unwind-protect sub 2.1: ~a\n" b) (set! b 2.2) (print "unwind-protect sub 2.2: ~a\n" b) (set! b 2.3) (print "unwind-protect sub 2.3: ~a\n" b)) (set! b 2.4))) (lambda () (unwind-protect (begin (set! c 3.1) (print "unwind-protect sub 3.1: ~a\n" c) (set! c 3.2) (print "unwind-protect sub 3.2: ~a\n" c) (set! c 3.3) (print "unwind-protect sub 3.3: ~a\n" c)) (set! c 3.4)))) (values a b c) ⇒ 1.4 2.4 3.4 -| unwind-protect sub 1.1: 1.1 -| unwind-protect sub 2.1: 2.1 -| unwind-protect sub 1.2: 1.2 -| unwind-protect sub 3.1: 3.1 -| unwind-protect sub 2.2: 2.2 -| unwind-protect sub 1.3: 1.3 -| unwind-protect sub 3.2: 3.2 -| unwind-protect sub 2.3: 2.3 -| unwind-protect sub 3.3: 3.3
Compensation handlers could be
evaluated in the dynamic environment of the call to push-compensation-thunk
,
by using with-current-dynamic-environment
. The current implementation of
compensations does not save the dynamic environment, because doing it is more
resource consuming.