Composable Promises: Adding Laziness to a Strict Language and Collapsing Indirection

:: racket, tutorials

By: Mike Delmonaco

Before there is any confusion, I’m not talking about JavaScript promises that are used for asynchronous computations. In this case, a promise is just a delayed computation. For example, a simple form of a promise is a function that takes in no arguments and returns a result. In this blog post, we will be focusing on promises that remember their results and promises that may evaluate to other promises. Promises are useful for control flow and implementing lazy semantics in a strict language.

In this blog post, we will learn what promises are and how to implement them efficiently. Promises are useful and interesting, but honestly, I mainly wrote this just to talk about the algorithm for forcing composable promises because I think it’s very cool!

1  What is a Promise?

A promise is a delayed computation. The computation executes at most once and remembers its result. Here are some examples of using promises:

> (define p (delay (println "in the delay") 2))
> (println "in top-level")

"in top-level"

> (force p)

"in the delay"

2

> (force p)

2

Notice that the "in the delay" is only printed after the promise is forced, not when the promise is created with delay. And the print does not run when we force the promise again. This tells us that the promise remembers its result and does not re-evaluate its body upon subsequent forces. In general, the body of a promise runs at most once. These simple promises can be implemented by storing the return value of a zero-argument function.

> (struct simple-promise (thunk [result #:mutable] [forced? #:mutable]))
> (define (make-simple-promise thunk) (simple-promise thunk #f #f))
> (define (force promise)
    (cond
      [(simple-promise-forced? promise) (simple-promise-result promise)]
      [else (let ([result ((simple-promise-thunk promise))])
              (set-simple-promise-result! promise result)
              (set-simple-promise-forced?! promise #t)
              result)]))
> (define-syntax-rule (delay body ...) (make-simple-promise (lambda () body ...)))
> (define p (delay (println "in the delay") 2))
> (println "in top-level")

"in top-level"

> (force p)

"in the delay"

2

> (force p)

2

If the promise has already been forced, we return the stored result. Otherwise, we call the thunk, store the result, and return it.

One practical use of promises is for lazy streams.

> (define empty-stream '())
> (define-syntax-rule (stream-cons a d) (cons a (delay d)))
> (define (stream-take s n)
          (cond [(or (null? s) (zero? n)) '()]
                [(= n 1) (list (car s))]
                [else (cons (car s) (stream-take (force (cdr s)) (sub1 n)))]))
> (define s1 (stream-cons 1 (begin (println "in the tail") (stream-cons 2 (error "boom")))))
> (stream-take s1 2)

"in the tail"

'(1 2)

> (stream-take s1 2)

'(1 2)

2  Composable Promises

What if you have a promise in a promise?

> (define p (delay (delay 2)))
> (force p)

#<promise:eval:1:0>

> (force (force p))

2

If you are using promises in a complicated way, you may end up producing promises inside of other promises like this. For example, let’s say you’re implementing an interpreter for a language with lazy semantics. If you use promises, you will likely end up producing deeply nested promises. lazy allows us to work with these nested promises:

> (define p (lazy (lazy (lazy 2))))
> (force p)

2

With lazy, forcing the outer promise forces all the intermediate promises and they all remember the inner result.

3  How Does it Work?

Let’s use the following chain of promises as a running example:

(define p4 (lazy (begin (sleep 5) "hello")))
(define p3 (lazy p4))
(define p2 (lazy p3))
(define p1 (lazy p2))

Here is a diagram of the situation:

p1 -?> p2 -?> p3 -?> p4 -?> (begin (sleep 5) "hello")

Here is a diagram of what we want after forcing p1:

p1 -> "hello"
p2 -> "hello"
p3 -> "hello"
p4 -> "hello"

In these diagrams, -?> represents an unforced promise and -> represents a forced promise.

How can we implement this? Naively, you’d write a function that just forces promises until you get a result. Something like this:

(define (force-deep p)
  (cond [(promise? p) (force-deep (force p))]
        [else p]))

Here is what the chain would look like after we used force-deep on p1:

p1 -> p2 -> p3 -> p4 -> "hello"

If we apply this function to p1, we will get back "hello". However, if we force p1 again, we will get p2, not "hello". If we use force-deep again, we will get "hello" and it will be faster than the first time since the result is remembered and the sleep would not run again. However, each promise stores its child, not the inner result. This means that when we use force-deep on p1 again, even though all the promises were already forced, we would still have to traverse the whole "chain" again just to access the inner result stored in the innermost promise. We want each promise to remember the inner result, not its child promise.

Here is another attempt, ignoring regular promises for the moment:

> (struct composable-promise (thunk [result #:mutable] [forced? #:mutable]))
> (define (make-composable-promise thunk) (composable-promise thunk #f #f))
> (define (force promise)
    (cond
      [(composable-promise-forced? promise) (composable-promise-result promise)]
      [else (let ([initial-result ((composable-promise-thunk promise))])
              (if (composable-promise? initial-result)
                  (let ([result (force initial-result)])
                    (set-composable-promise-result! promise result)
                    (set-composable-promise-forced?! promise #t)
                    result)
                  (begin (set-composable-promise-result! promise initial-result)
                         (set-composable-promise-forced?! promise #t)
                         initial-result)))]))
> (define-syntax-rule (lazy body ...) (make-composable-promise (lambda () body ...)))
> (define p (lazy (lazy (displayln "hello") 2)))
> (force p)

hello

2

> (composable-promise-result p)

2

> (force p)

2

As before, if the promise has already been forced, we return the stored result. Otherwise, we start by calling the thunk to get an initial result. If that is yet another composable promise, we force that one too. Then we store and return that promise’s result. If the thunk returned a regular value, we store and return it. The recursive case will go all the way down the chain, forcing every inner promise and storing the inner value, not the inner promise itself.

This is better. If we have a chain of promises and we force it, each promise will remember its result. Here is the diagram after forcing:

p1 -> "hello"
p2 -> "hello"
p3 -> "hello"
p4 -> "hello"

That’s exactly what we want! Forcing p1 once more will immediately return the stored result and wouldn’t have to traverse the chain again. But there is still one problem: that (force initial-result) does not occur in tail position. This means that the forcing algorithm will use stack space that grows linearly with respect to the chain length.

How can we implement this with tail recursion?

Consider this diagram:

a -?> b -?> c -?> ...

Let’s say we shallowly force a.

a -> b -?> c -?> ...

We realize that a results in another promise, so we shallowly force that too.

a -> b -> c -?> ...

Next, we swap a and b.

b -> a -> c -?> ...

Then, we repeat from a.

shallowly force it.

b -> a -> c -?> ...

It was already forced, so nothing changed. Next, we realize that the result is another promise, namely c. So we shallowly force that too.

b -> a -> c -> ...

Next, swap a and c.

b -> a -> ...
     
     c

Then, we repeat from a and continue until a points to a non-promise or its child points to a non-promise. In either of those two cases, we store the non-promise.

Here is what we’d get if we ran this algorithm on p1:

p2 -> p1 -> "hello"
           
      p3    p4

In general, the pattern will be

p2   \\
... - * -> p1 -> v
pn-1 //          
                 pn

Intermediate promises will store the outer-most promise, the outer-most promise will store the final value, and the inner-most promise will store the final value. Each promise is at most 2 levels of indirection away from the final value. If we forced p2 from this example after forcing p1, it would end up pointing directly to the final value.

This is not quite what we wanted. We wanted all promises to store the final value directly. Realistically, however, a single layer of indirection for inner promises doesn’t matter. Especially when it would end up being collapsed if an intermediate promise was forced afterwards. Asymptotically, we still get linear time with respect to chain length on the first force and constant time on any subsequent force on a promise in the chain. That’s the same as our naive algorithm. However, our naive algorithm used linear space, whereas this algorithm uses constant space. This is a tradeoff.

Either way, I think this algorithm is pretty cool! In a single, linear-time, constant-space pass, we collapse the indirection. Here is the code:

> (struct composable-promise (thunk [result #:mutable] [forced? #:mutable]))
> (define (make-composable-promise thunk) (composable-promise thunk #f #f))
> (define (shallow-force promise)
    (cond
      [(composable-promise-forced? promise) (composable-promise-result promise)]
      [else (let ([result ((composable-promise-thunk promise))])
              (set-composable-promise-result! promise result)
              (set-composable-promise-forced?! promise #t)
              result)]))
> (define (force promise)
    (let ([result (shallow-force promise)])
      (cond
        [(composable-promise? result)
         (let ([inner-result (shallow-force result)])
           (cond
             [(composable-promise? inner-result)
  
              (set-composable-promise-result! result promise)
              (set-composable-promise-result! promise inner-result)
              (force promise)]
             [else
              (set-composable-promise-result! promise inner-result)
              inner-result]))]
        [else result])))
> (define-syntax-rule (lazy body ...) (make-composable-promise (lambda () body ...)))
> (define p4 (lazy (displayln "hello") 42))
> (define p3 (lazy p4))
> (define p2 (lazy p3))
> (define p1 (lazy p2))
> (force p1)

hello

42

> (composable-promise-result p1)

42

> (composable-promise-result p2)

#<composable-promise>

> (eq? p1 (composable-promise-result p2))

#t

> (eq? p1 (composable-promise-result p3))

#t

> (composable-promise-result p4)

42

> (force p1)

42

We can examine the stored results of the promises directly to see that it does, in fact, produce the graph we predicted.

This implementation is missing a lot of things. There are no regular promises, there is no error handling, there is no support for values, and many more subtle things the real promise library handles. Regardless, this provides insights into the inner workings of the library and an interesting algorithm for collapsing indirection.