Email list hosting service & mailing list manager


Getting rid of the delay-force aid Marc Nieper-Wißkirchen 16 Jul 2017 12:58 UTC

(This is in part a continuation of the post
https://srfi-email.schemers.org/srfi-155/msg/6070271.)

In this post I describe a backward-compatible superior semantics for
delayed evaluation that makes ‘delay-force‘ unnecessary. Despite the
length of this post, I would like to get some feedback from users of
delayed evaluation in Scheme and from implementers.

Since the R5RS, delayed evaluation has become part of a Scheme standard.
The primitives defined by the R5RS are ‘delay’ and ‘force‘. At least
since SRFI 45 it was known that the obvious implementation of ‘delay’
and ‘force‘ is not sufficient for expressing iterative lazy algorithms.
The solution of SRFI 45 was to add a third primitive, later named
‘delay-force’ in the R7RS. The observable behavior of ‘(delay-force
<expression>)’ is the same as of ‘(delay (force <expression>))’ except
that the former allows an unbounded number of active delay-force
expressions in tail position.

From a user's perspective, the need for this third primitive is not
optimal, and one may think that a Scheme system properly supporting
delayed evaluation should handle ‘(delay (force <expression>))' as
gracefully as it does handle calls in tail position (as described in 3.5
of the R7RS).

It was suggested by Alex Shinn that ‘delay‘ should be redefined along
the following lines:

(define-syntax delay
  (syntax-rules (force)
   ((delay (force expression))
    (delay-force expression)
   ((delay expression)
    ...)))

With this definition, during expansion of ‘delay’ the system statically
detects whether the expression to be delayed is of the form ‘(force
<expression>)’. This means that ‘delay-force’ does not have to be
exposed anymore and that a Scheme user can write ‘(delay (force
<expression>))’ instead of ‘(delay-force <expression>)‘.

Besides probably being more user-friendly, this redefinition of ‘delay’
makes the delayed evaluation model also more composable, hence more
powerful: Consider the following example code (using SRFI 111):

(define-syntax lazy-box
  (syntax-rules ()
    ((lazy-box expression)
     (box (delay expression)))))

(define-syntax lazy-unbox
  (syntax-rules ()
   ((lazy-unbox box)
    (force (unbox box)))))

The lazy boxes defined by these two keywords wrap a promise in a box.
The pair of ‘lazy-box’ and ‘lazy-unbox’ provides the same primitives for
lazy boxes as do ‘delay’ and ‘force’ for non-boxed promises. Using the
suggested semantics that ‘(delay (force <expression>))’ is equivalent to
‘(delay-force <expression>)’, the composition ‘(lazy-box (lazy-unbox
<expression>))’ reduces correctly and makes expressing iterative lazy
algorithms with lazy boxes possible.

This is also the reason why ‘lazy-unbox’ has to be defined as syntax and
not as a procedure. Had we defined ‘lazy-unbox’ as a procedure, the call
to ‘force’ in tail position would have remained hidden from the syntax
expander.

While the automatic syntactic reduction of ‘(delay (force
<expression>))’ to ‘(delay-force <expression>)’ is, as this example
shows, better than exposing ‘delay-force’ directly, this necessity of
having to define ‘lazy-unbox’ as syntax also shows that the proposed
semantics is still a suboptimal choice.

From a user's perspective, this handling of ‘(delay (force
<expression>))’ is still not as graceful as it should be. The problem is
that the previous solution is based on detecting the presence of ‘force’
syntactically. Semantically, however, it is more correct, to reduce
‘(delay <expression>)’ to a ‘delay-force’-expression at execution time,
namely whenever a call to ‘force’ happens in ‘<expression>’ in tail
position.

This desirable semantics can be achieved with continuation marks that
are yet not part of the Scheme language, but viable candidates for (in
particular because they make proper tail recursion observable). Racket
is a dialect of Scheme that already exposes continuation marks to the
user. With the primitives for continuations provided by Racket and
otherwise portable R5RS code, the primitives of delayed evaluation of
the R7RS library ‘(scheme lazy)’ can be coded as follows:

#lang racket

;;; Internal definitions

(define %promise (vector 'promise))

(define (%make-promise done? value)
  (vector %promise (vector done? value)))

(define (%promise-content promise)
  (vector-ref promise 1))

(define (%promise-set-content! promise content)
  (vector-set! promise 1 content))

(define (%promise-done? promise)
  (vector-ref (%promise-content promise) 0))

(define (%promise-value promise)
  (vector-ref (%promise-content promise) 1))

(define (%promise-set-done?! promise done?)
  (vector-set! (%promise-content promise) 0 done?))

(define (%promise-set-value! promise value)
  (vector-set! (%promise-content promise) 1 value))

(define (%promise-update! new old)
  (%promise-set-done?! old (%promise-done? new))
  (%promise-set-value! old (%promise-value new))
  (%promise-set-content! new (%promise-content old)))

;;; Exported syntax

(define-syntax delay
  (syntax-rules ()
    ((delay expr)
     (%make-promise #f (lambda () expr)))))

(define-syntax delay-force
  (syntax-rules ()
    ((delay-force promise)
     (delay (force promise)))))

;;; Exported procedures

(define (promise? obj)
  (and (vector? obj)
       (> (vector-length obj) 0)
       (eq? %promise (vector-ref obj 0))))

(define (make-promise obj)
  (%make-promise #t obj))

(define (force promise)
  (if (%promise-done? promise)
      (%promise-value promise)
      (call-with-immediate-continuation-mark
       %promise
       (lambda (c)
         (if c
             (c promise)
             (let ((promise*
                    (call-with-current-continuation
                     (lambda (c)
                       (make-promise
                        (with-continuation-mark %promise c
                          ((%promise-value promise))))))))
               (unless (%promise-done? promise)
                 (%promise-update! promise* promise))
               (force promise)))))))

This implementation passes all tests of SRFI 45. That it has the desired
semantics can be seen from the definition of ’delay-force’. (For
simplicity, it ignores questions of dynamic extent as addressed by this
SRFI.)

This SRFI 155 wants to get the promises of the R7RS "right". So far, it
only requires from implementations to syntactically rewrite ‘(delay
(force <expression>))’ into ‘(delay-force <expression>)’. It does not
yet require to detect calls to ‘force‘ in tail position. By the
discussion from above, however, this would mean that SRFI 155 would get
the promises of the R7RS only "sort of right".

Thus I am inclined to make the optimal semantics from above a must-have
for implementations of this SRFI. In particular, ‘delay-force’ would
have outlived its usefulness. From a user's perspective, this is
certainly the way to go. From an implementer's perspective, this new
requirement may force him or her to add a few new primitives for
inspection of continuations or the stack (like continuation marks, which
are interesting on their own).

In order to make the sample implementation of this post more
self-contained, I am also considering to write a SRFI providing the
notion of continuation marks and the necessary primitives.

--

Marc