4

I'm trying to port yield and yield from from Python to Scheme.

Here is an implementation I've done:

(define (coroutine routine)
  (let ((current routine)
    (status 'new))
    (lambda* (#:optional value)
      (let ((continuation-and-value
         (call/cc (lambda (return)
            (let ((returner
                   (lambda (value)
                 (call/cc (lambda (next)
                        (return (cons next value)))))))
              (if (equal? status 'new)
                  (begin
                (set! status 'running)
                (current returner))
                  (current (cons value returner)))
              (set! status 'dead))))))
    (if (pair? continuation-and-value)
        (begin (set! current (car continuation-and-value))
           (cdr continuation-and-value))
        continuation-and-value)))))

The problem, with this implementation is that the way it has to be called doesn't looks like Python's yield.

(define why (call/cc (lambda (yield)
               (format #t "love me or leave me!")
               (yield "I leave!")
               ;; the program never reach this part
               (format #t "it probably left :("))))
(format #t "return actually populates WHY variable\n")
(format #t "WHY: ~a\n")

Among other things, each time I need to-restart the coroutine, I must let a new return variable to be able exit the coroutine. Basically, I find the syntax too verbose. Is there another to have cleaner syntax?

It should be possible to yield and send values to the coroutine. Here is an example of how the coroutine must be used:

(define-coroutine (zrange start step)
  "compute a range of values starting a START with STEP between
   each value. The coroutine must be restarted with 0 or more, which
   is added to the step"
  (let loop ((n start))
    (loop (+ n step (yield n)))))


(coroutine-map (zrange 0 10) '(1 100 1000 10000 100000))
;; => 0 110 1120 11130 111140

In the above, 1 is ignored and then 100, 1000 are send to the generator. I've done an implementation, based on @sylwester code, but I have troubles with the macro:

(define (make-generator procedure)
  (define last-return #f)
  (define last-value #f)
  (define last-continuation (lambda (_) (procedure yield)))

  (define (return value)
    (newline)(display "fuuu")(newline)
    (call/cc (lambda (continuation)
               (set! last-continuation continuation)
               (set! last-value value)
               (last-return value))))
  (lambda* (. rest)  ; ignore arguments
    (call/cc (lambda (yield)
               (set! last-return yield)
               (apply last-continuation rest)))))

(define-syntax define-coroutine
  (syntax-rules ()
    ((_ (name args ...) body ...)
     (define (name args ...)

       (make-generator
        (lambda (yield)
          body ...))))))

(define-coroutine (zrange start step)
  (let loop ((n start))
     (loop (+ n step (yield n)))))

(display (map (zrange 0 10) '(1 100 1000 10000 100000)))
amirouche
  • 7,682
  • 6
  • 40
  • 94
  • What is `coroutine-map`? where in `zrange` do you get the argument? – Sylwester Jun 08 '15 at 08:49
  • Which argument? ``yield`` is not an argument of zrange. I think it requires unhygenic macros. – amirouche Jun 08 '15 at 11:37
  • ``coroutine-map`` iterates over the values returned by (zrange 0 10) until some error. – amirouche Jun 08 '15 at 11:37
  • How does your `coroutine-map` know that is should `+` the elements together? What if you wanted to multiply? with arguments I'm referring to `send` can you send more values to `zrange` if it had a finit length? Would it be like `yielding` each one in order at the bottom? – Sylwester Jun 08 '15 at 12:22
  • when you `send` something, the generator restarts and `yield` "returns" the value that was sent. That's why `(+ n step (yield n))` becomes `(+ 0 10 100)`. I just figured that the first value of the map is not taken in to account in my implementation. I will add the implementation I've done. – amirouche Jun 08 '15 at 16:57
  • Because of hygiene the `yield` introduced in your macro is not the same as the one in the body. Instead of `( . rest)` you just want `rest`. I think I know what you want so I'll update my answer. – Sylwester Jun 08 '15 at 20:43
  • That question was misguided, see https://stackoverflow.com/q/56317339/140837 – amirouche May 26 '19 at 21:02

3 Answers3

5

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
Sylwester
  • 47,942
  • 4
  • 47
  • 79
  • what is ``collect`` ? Otherwise, it's basically what I'm look for. – amirouche Jun 08 '15 at 08:31
  • There is a feature missing I'll add it in the question. – amirouche Jun 08 '15 at 08:33
  • @amirouche make-generator takes a procedure that accepts the yield-procedure as argument, just as call/cc takes a procedure that accepts a continuation procedure as argument. Thus you get to choose in your generator what you want `yield` to be called since you use whatever the argument name is. – Sylwester Jun 08 '15 at 08:43
  • I understand now. Guile was complaining about a deprecated feature. I added an example of what I am exactly looking for. – amirouche Jun 08 '15 at 08:45
  • Thanks it's ok! I will try to do the implementation using shift reset myself. – amirouche Jun 10 '15 at 15:01
  • @amirouche Beware that `shift` and `reset` are not a part of the Scheme standard so you will be making something that is unportable across implementations. eg, it won't run on Racket since they have their own delimited continuation function names. – Sylwester Jun 10 '15 at 15:48
2

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)?

Community
  • 1
  • 1
Chris Vine
  • 677
  • 3
  • 7
  • Thanks. I though shift/reset was the best way to replace call/cc? – amirouche Jun 10 '15 at 14:59
  • 1
    Generally, for delimited continuations with guile, prompts are the way to go, and I doubt that your own implementation using shift/reset will be an improvement over guile's call-with-prompt and abort-to-prompt. prompts are the way guile implements exceptions as well as escape continuations. I should start with guile's prompts and see if you can improve on them, but for this usage (coroutine generators) I doubt you will. – Chris Vine Jun 10 '15 at 20:20
2

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
QuesterZen
  • 121
  • 5
  • A variation of this trick is used by the struct macro to define new names in the global environment and I also use that quite a bit. If you have DrRacket, I would recommend looking at the source code for some of the macros: they are well written and you can pick up a lot of tricks that aren't easy to learn from the Scheme or Racket documentation otherwise. See https://stackoverflow.com/questions/20931806/is-struct-a-macro-in-racket/54120909#54120909 – QuesterZen Feb 08 '19 at 01:51