© Copyright Kirk Rader 2023. All rights reserved.
dynamic-wind
The built-in dynamic-wind
procedure is Scheme's equivalent to Common Lisp's
unwind-protect
or the try ... finally ...
construct in languages like C#,
Java or JavaScript. The name of Scheme's version takes into account that certain
code blocks must be protected not only when the stack is being "unwound" upon
exit from a function but also, thanks to first-class continuations, when the
stack is being "rewound" upon re-entry. Both unwind-protect
and try... finally...
(the latter was modeled on the former) define two sections of code:
a section being protected and a section that is guaranteed to execute when the
protected section is exited, even in case of exit due to non-sequential flow of
control. Scheme's dynamic-wind
adds a third section that is guaranteed to
execute before the protected section begins, no matter how many times control
flows in and out of it.
;; before-thunk will execute before protected-thunk
;; every time protected-thunk's body is entered
;;
;; after-thunk will execute after protected-thunk
;; every time protected-thunk exits
;;
;; dynamic-wind returns whatever is returned by
;; protected-thunk each time it exits
(dynamic-wind before-thunk protected-thunk after-thunk)
X-Ray Flourescence (XRF) spectrometry is used to determine the composition of
materials, for example in scrap yards and recyclying facilities. An industrial
XRF scanner can emit levels of radiation that are quite dangerous if, for
example, someone were to expose a hand or other body part to it. Imagine a
program to control such a device. It might have a function, scan-sample
as the
main entry-point, with the following requirements:
-
The sample chamber must have a door which prevents users from reaching inside when it is locked
-
The X-Ray emitter can only be turned on when the door is locked
-
The XRF spectrogram is recorded and made available to the user after a successful scan
One can imagine helper functions, called by scan-sample
, for each of these operations:
;; naive (unsafe!) implementation of scan-sample
(define (scan-sample)
(lock-door)
(energise-emitter)
(record-data)
(de-energize-emitter)
(unlock-door))
As long as everything proceeds as expected, the naive definition implements the requirements for scanning a sample laid out above. But sometimes unexpected things can happen. Imagine that the XRF scanner is able to detect when a sample must be repositioned within the chamber in order to obtain a complete spectrogram. In that case, it needs to be able to interrupt the scanning process, request that the user reposition the material within the chamber and then resume from where it left off. Scheme continuations can be used to support such requirements.
What follows is a complete implementation of a simulation of scan-sample
that
uses stdout
to log the simulated operations. It uses continuations to throw
resumable exceptions when user intervention is required together with
dynamic-wind
to ensure that the emitter is always off when the door is
unlocked.
;; demonstrates:
;;
;; - continuations for non-sequential flow-of-control
;;
;; - dynamic-wind to protect blocks of code when using non-sequential flows-of-control
;;
;; - locally defined variables for encapsulation
;;
;; - macros for modularity
;; note from the output that the emitter is only on when the door is locked and the
;; sample is only repositioned when the door is unlocked. note also that the counter
;; shows that not only are the safety constraints being respected, the operations are
;; being done in the correct order
(define (scan-sample)
(let ((display-message (lambda (message sample)
;; common helper that prints a message to stdout
(display (string-join (list message (number->string sample) "\n") "")))))
;; deliberately not mutually-callable procedures representing various states of the xrf
;; spectrometer hardware
(let ((lock-door (lambda ()
;; simulate locking the sample chamber door
(display "door locked\n")))
(energize-emitter (lambda ()
;; simulate turning on the x-ray emitter
(display "emitter energized\n")))
(de-energize-emitter (lambda ()
;; simulate turning off the x-ray emitter
(display "emitter de-energized\n")))
(unlock-door (lambda ()
;; simulate unlocking the sample chamber door
(display "door unlocked\n")))
(record-data (lambda (sample)
;; simulate recording a xrf spectrogram
(call/cc (lambda (return)
(display-message "scanning " sample)
(display "please reposition sample\n")
(set! sample (call/cc (lambda (resume) (return resume))))
(display-message "scanning " sample)
(display "please reposition sample\n")
(set! sample (call/cc (lambda (resume) (return resume))))
(display-message "scanning " sample)
(display "data recorded\n"))))))
(let-syntax ((with-emitter-energized (syntax-rules ()
;; ensure x-ray emitter is on only while the protected
;; forms are executing
((with-emitter-energized protected ...)
(dynamic-wind
energize-emitter
(lambda () protected ...)
de-energize-emitter))))
(with-door-locked (syntax-rules ()
;; ensure the sample chamber door is locked while the
;; protected forms are executing
((with-door-locked protected ...)
(dynamic-wind
lock-door
(lambda () protected ...)
unlock-door)))))
(letrec ((count 1)
(resume (with-door-locked (with-emitter-energized (record-data count)))))
;; keep scanning and following prompts to reposition the sample until record-data
;; signals it is finished by not returning a continuation
(if (procedure? resume)
(begin
(display-message "repositioning " count)
(set! count (+ count 1))
(resume count))
(display-message "samples scanned: " count)))))))
See xrf.scm
While all of the subroutines and helpers are defined inside the body of
scan-sample
for encapsulation, the various let
, let-syntax
and letrec
forms are nested in ways that also helps enforce the requirements. In
particular, none of the main subroutines, lock-door
, energize-emitter
and so
on, can call one another because they are all deliberately defined in a single
let
(not letrec
). Only the display-message
helper is visible to the
entire body of scan-sample
because it is in its own outermost let
. The only
letrec
is the innermost scope, and it is used instead of let
only to allow
the invocation of record-data
to receive count
as a parameter without having
to introduce yet another level of lexical scope nesting.
Here is the result of invoking (scan-sample)
:
> (scan-sample)
door locked
emitter energized
scanning 1
please reposition sample
emitter de-energized
door unlocked
repositioning 1
door locked
emitter energized
scanning 2
please reposition sample
emitter de-energized
door unlocked
repositioning 2
door locked
emitter energized
scanning 3
data recorded
emitter de-energized
door unlocked
samples scanned: 3
As can be seen from the output, the correct set of operations are performed, in
the correct order relative to one another so as to conform to the safety
requirements. The numeric counter shows that the operations are also carried out
in the correct order in regards to the end-to-end flow. This means that
record-data
is actually completely sequential from its own point of view while
the body of scan-sample
uses the invocation of a continuation in tail
position to turn itself into a loop. The invocations of the
before and after logic by the various subroutines is encapsulated in the
with-door-locked
and with-emitter-energized
special forms.