Search code examples
functional-programmingschemeracketcoroutinecontinuations

Deadlock in Racket co-routine implementation


As a project to help me understand continuations in Racket, I decided to try and write a co-routine implementation without using mutable or global variables. Here is what I have so far, but it seems to end up in a deadlock of some kind. Am I missing something obvious?

#!/usr/bin/env racket

#lang racket

(define (make-proc-args args)
  (let/cc cc
    (cons cc args)))

(define (fork proc)
  (let ([current (make-proc-args '())])
    (proc current)))

(define (yield to args)
  (let ([current (make-proc-args args)])
    ((car to) current)))

(define c
  (fork
    (lambda (p)
      (let loop ([i 0]
                 [parent p])
        (unless (> i 10)
          (loop (+ i 1) (yield parent (list i))))))))

(let loop ([child c])
  (println (car child))
  (loop (yield child '())))

Solution

  • (define (make-proc-args args)
      (let/cc cc
        (cons cc args)))
    

    This when called returns it's continuation as a object. If you look at this code:

    (define (fork proc)
      (let ([current (make-proc-args '())])
        (proc current)))
    

    The continuation of (make-proc-args '()) is the application of the let with current bound and proc called. In the context of:

    (fork
     (lambda (p)
       (let loop ([i 0]
                  [parent p])
         (unless (> i 10)
           (loop (+ i 1) (yield parent (list i)))))))
    

    It means (yield parent (list i)) will time travel back and call and (proc current) will be called again.. The let with start with i and 0.. But one would expect the continuation of the yield to be stored, right? Wrong!

    (define (yield to args)
      (let ([current (make-proc-args args)])
        ((car to) current)))
    

    The continuation that is captured is ((car to) current) which happen to be the same again and again and again.

    The easiest way to solve this is to make the continuation have not the calling of your stored continuation as it's own continuation. Thus you need to do something like this:

    (define (fork proc)
      (let/cc cc
        (let ([current (cons cc '())])
          (proc current))))
    
    (define (yield to args)
      (let/cc cc
        (let ([current (cons cc args)])
          ((car to) current))))
    

    Notice that in both of these the continuation is what happens when yield and fork returns naturally and not when the body of a let is finished.

    Also know that continuations are delimited at top level so you should perhaps test with all code in a let block to catch bugs you might have since continuations behave differently at top level. The define is not allowed top level, but if you put it in a let you get #<void> as the last value as child because that is the value define forms make, not the pairs you expect.

    (define (worker p)
      (let loop ([i 0]
                 [parent p])
        (unless (> i 10)
          (loop (+ i 1) (yield parent i)))))
    
    (let ((c (fork worker)))
      (let loop ([child c])
        (when (pair? child)
          (println child)
          (loop (yield child '())))))
    

    This prints:

    (#<continuation> . 0)
    (#<continuation> . 1)
    (#<continuation> . 2)
    (#<continuation> . 3)
    (#<continuation> . 4)
    (#<continuation> . 5)
    (#<continuation> . 6)
    (#<continuation> . 7)
    (#<continuation> . 8)
    (#<continuation> . 9)
    (#<continuation> . 10)
    

    As a last tip. Perhaps you should make a struct for your continuation object or at least abstract?