Implement yield and send in Scheme

扶醉桌前 提交于 2019-11-27 23:19:47

Something like this:

(define (make-generator procedure)
  (define last-return values)
  (define last-value #f)
  (define (last-continuation _) 
    (let ((result (procedure yield))) 
      (last-return result)))

  (define (yield value)
    (call/cc (lambda (continuation)
               (set! last-continuation continuation)
               (set! last-value value)
               (last-return value))))

  (lambda args
    (call/cc (lambda (return)
               (set! last-return return)
               (if (null? args)
                   (last-continuation last-value)
                   (apply last-continuation args))))))

Used like this:

(define test 
 (make-generator
   (lambda (collect)
     (collect 1)
     (collect 5)
     (collect 10)
     #f)))

(test) ; ==> 1
(test) ; ==> 5
(test) ; ==> 10
(test) ; ==> #f (procedure finished)

Now we can wrap the internals into a macro:

(define-syntax (define-coroutine stx)
  (syntax-case stx ()
    ((_ (name . args) . body )
     #`(define (name . args)
         (make-generator 
          (lambda (#,(datum->syntax stx 'yield))
            . body))))))

Notice that define-coroutine is implemented using syntax-case since we need to make yield unhygienic.

(define-coroutine (countdown-from n)
  (let loop ((n n))
    (if (= n 0)
        0
        (loop (- (yield n) 1)))))

(define countdown-from-10 (countdown-from 10))

(define (ignore procedure)
  (lambda ignore
    (procedure)))

(map (ignore countdown-from-10) '(1 1 1 1 1 1)) ; ==> (10 9 8 7 6 5)

;; reset
(countdown-from-10 10)  ; ==> 9
(countdown-from-10)     ; ==> 8
;; reset again
(countdown-from-10 100) ; ==> 99

Kudos to @Sylwester for a great answer.

The difficult part is making yield available to the generator function. datum->syntax creates a syntax object, and requires you to provide another syntax object from which the context for the new object is taken. In this case, we can use stx which has the same context as the function passed into the macro.

If people find it helpful, I use a simpler version:

(define-syntax (set-continuation! stx)
  "Simplifies the common continuation idiom
    (call/cc (λ (k) (set! name k) <do stuff>))"
  (syntax-case stx ()
    [(_ name . body)
     #`(call/cc (λ (k)
                  (set! name k)
                  . body))]))

(define-syntax (make-generator stx)
  "Creates a Python-like generator. 
   Functions passed in can use the `yield` keyword to return values 
   while temporarily suspending operation and returning to where they left off
   the next time they are called."
  (syntax-case stx ()
    [(_ fn)
     #`(let ((resume #f)
             (break #f))
         (define #,(datum->syntax stx 'yield)
           (λ (v)
             (set-continuation! resume
               (break v))))
         (λ ()
           (if resume
               (resume #f)
               (set-continuation! break
                 (fn)
                 'done))))]))

An example of its usage:

(define countdown
  (make-generator
   (λ ()
     (for ([n (range 5 0 -1)])
           (yield n)))))

(countdown)
=> 5
...
(countdown)
=> 1
(countdown)
=> 'done
(countdown)
=> 'done
Chris Vine

One approach is here. If you are using guile, you should use prompts (they are about two orders of magnitude faster than using full continuations with guile):

How to implement Python-style generator in Scheme (Racket or ChezScheme)?

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!