dynamic-wind

dynamic-wind

The built-in dynamic-wind procedure is Scheme’s equivalent to Common Lisp’s unwind-protect, the try ... finally ... construct in languages like C# and Java and so on.

The main entry point for this example is a provedure named drill-hole. It uses the simulated drill press described earlier. The drill-hole function defines several private functions including start-drilling, keep-drilling and stop-drilling that are used in the arguments passed to dynamic-wind in the body of drill-hole. As noted in the chapter on [lexical closures][] where [make-drill][] was defined, the simulated drill press has a number of run-time constraints such as that the drill motor can only be started or stopped when the drill height is 0. The drill-hole procedure is a wrapper for an instance of such a simulated drill that uses dynamic-wind to protect the wrapped drill from violations of such constraints even when its keep-drilling loop is exited and re-entered using a continuation.

The result of calling keep-drilling is bound to the local variable k in the body of drill-hole. That call to keep-drilling is protected during stack winding and unwinding by calls to start-drilling and stop-drilling, respectively, by passing all three functions in a call to dynamic-wind.

Those familiar with languages with constructs like (unwind-protect ...) or try ... finally ... can understand the relationship between keep-drilling and stop-drilling as being an enhanced version of such constructs.

In other words, dynamic-wind guarantees to call stop-drilling after keep-drilling exits, whether or not the latter exits normally or through some non-sequential flow of control, as in the following Common Lisp code:

(unwind-protect

  (progn
     (start-drilling)
     (keep-drilling))

  (stop-drilling))

Or in the case of languages like Java and C#, something like:

try {

    startDrilling();
    keepDrilling();

} finally {

    stopDrilling();

}

In C++, similar stack-unwinding protections are achieved using “automatic” variables of types with destructors.

The additional functionality offered by dynamic-wind over the unwind-protect and try ... finally ... examples is that dynamic-wind also guarantees that start-drilling will be called before keep-drilling starts execution the first time it is entered and if it is re-entered by invoking a continuation after it has previously exited.

This stack winding / unwinding / rewinding behavior is demonstrated in the body of drill-hole. In particular, keep-drilling bores the hole 50% through the full range of the possible bit positions and then exits, returning a continuation that is later used to resume drilling until the hole is 100% complete. Looking at the output, one can see that start-drilling and stop-drilling are called the appropriate number of times (twice each), in the appropriate places in the overall flow of control (each time the body of keep-drilling is about to be entered and after each time keep-drilling exits, respectively).

Here is the Scheme source for drill-hole:

;; -*- geiser-scheme-implementation: guile -*-

;; Copyright 2016 Kirk Rader
;;
;; Licensed under the Apache License, Version 2.0 (the "License"); you
;; may not use this file except in compliance with the License.  You
;; may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
;; implied.  See the License for the specific language governing
;; permissions and limitations under the License.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A Scheme program that illustrates the use of dynamic-wind to
;; protect entry, exit and re-entry of an execution context using
;; continuations.
;;
;; This uses an object that simulates an automated drill press. The
;; simulated drill has various operational constraints, such as that
;; the main drilling motor can only be turned on or off when the bit
;; is in its highest position.
;;
;; The point of the example is to show how dynamic-wind can be used to
;; ensure that these constraints are taken into account by an ongoing
;; sequence of drilling commands even when that sequence might be
;; exited and then re-entered using continuations.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Function: (drill-hole)
;;
;; Usage: (drill-hole)
;;
;; Outputs a sequence of messages to the console indicating a drill's
;; state changes over the course of a program to bore a hole.
;;
;; This uses dynamic-wind to ensure that the drilling motor is safely
;; started and stopped each time the drilling program execution
;; context is entered, exited or re-entered using continuations.
;;
;; Returns a value that indicates the outcome of the drilling program.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (drill-hole)

  (let ((drill (make-drill)))

    (letrec ((start-drilling
              ;; turn on the motor
              ;;
              ;; assumes that motor is currently stopped and that bit
              ;; position is 0
              ;;
              ;; this will be used as the "before thunk" in a call to
              ;; dynamic-wind
              (lambda ()
                (display "start-drilling called")
                (newline)
                (drill 'start-motor!)))

             (stop-drilling
              ;; raise the bit and stop the motor
              ;;
              ;; assumes that motor is currently running
              ;;
              ;; this will be used as the "after thunk" in a call to
              ;; dynamic-wind
              (lambda ()
                (display "stop-drilling called")
                (newline)
                (drill 'move-bit! 0)
                (drill 'stop-motor!)))

             (keep-drilling
              ;; the drilling program
              ;;
              ;; assumes the motor is currently running
              ;;
              ;; stops half-way through to let the bit cool, returning a
              ;; continuation by which drilling can be resumed and
              ;; continue to completion
              ;;
              ;; returns 'done when the hole is completely drilled
              (lambda ()
                (call/cc
                 (lambda (return)
                   (display "keep-drilling entered")
                   (newline)
                   (drill 'move-bit! 50)
                   (display "hole 50% drilled, pausing to let bit cool")
                   (newline)
                   (let ((command (call/cc (lambda (k) (return k)))))
                     (display "keep-drilling re-entered")
                     (newline)
                     (if (eq? command 'continue)
                         (begin
                           (display "continuing to drill")
                           (newline)
                           (drill 'move-bit! 100)
                           (display "hole completely drilled")
                           (newline)
                           'done)
                         (begin
                           (display "drilling program canceled")
                           (newline))))))))
         
             (k
              ;; bind k to the result of a call to keep-drilling,
              ;; protected during stack winding and unwinding by calls to
              ;; start-drilling and stop-drilling, respectively
              (dynamic-wind

                ;; ensure drill motor is running every time keep-drilling
                ;; is entered or re-entered
                start-drilling

                ;; drill the hole
                keep-drilling

                ;; ensure drill motor is stopped every time keep-drilling
                ;; exits
                stop-drilling)))

      ;; we get here twice:
      ;;
      ;; 1. when keep-drilling returns a continuation half-way through
      ;; the drilling process
      ;;
      ;; 2. when keep-drilling ultimately completes as a result of the
      ;; invocation of k, below
      ;;
      ;; in *both* cases, the preceding call to dynamic-wind ensures
      ;; that start-drilling is called before the body of
      ;; keep-drilling is entered and stop-drilling is called after
      ;; the body of keep-drilling exits, even though those entries
      ;; and exits happen at different times and at different points
      ;; in the flow of control

      (if (procedure? k)

          ;; k is a continuation procedure when keep-drilling exits
          ;; half-way throuh the drilling process
          (begin
            (display "waiting a bit (pun intended)")
            (newline)
            ;; use k to resume drilling
            ;;
            ;; keep-drilling will not exit again until the hole is
            ;; completely bored unless an exception is thrown
            (k 'continue))

          ;; when keep-drilling finishes boring the hole completely, it
          ;; returns a value that is not a procedure
          ;;
          ;; return that final value as the result of drill-hole
          k))))

(download drill-hole.scm)

Here is the output of (drill-hole):

start-drilling called
started motor
keep-drilling entered
stepping bit position from 0 to 50 by 1
hole 50% drilled, pausing to let bit cool
stop-drilling called
stepping bit position from 50 to 0 by -1
motor stopped
waiting a bit (pun intended)
start-drilling called
started motor
keep-drilling re-entered
continuing to drill
stepping bit position from 0 to 100 by 1
hole completely drilled
stop-drilling called
stepping bit position from 100 to 0 by -1
motor stopped