PythonライクなGeneratorをSchemeで作ってみた

並列プログラミングについて考えるため、'thread', 'coroutine', 'fiber', 'generator' などを調べていました。その途中でPythonのGeneratorについて書いてある http://linuxgazette.net/100/pramode.html に行き当たり、「なるほどこれは便利なものだな!」と感心したので、ちょっとSchemeで書いてみることにしました。

ライブラリ (tidbits generator)

;;; Ypsilon Scheme System
;;; Copyright (c) 2004-2008 Y.FUJITA, LittleWing Company Limited.
;;; See license.txt for terms and conditions of use.

(library (tidbits generator)
  (export define-generator)
  (import (rnrs) (only (core) format))

  (define unexpected-return
    (lambda (name)
      (assertion-violation
       'iteration
       (format "unexpected return of generator function ~s" name))))
      
  (define-syntax define-generator
    (lambda (x)
      (syntax-case x (lambda)
        ((stx name (lambda formals e0 e1 ...))
         (with-syntax ((yield (datum->syntax #'stx 'yield)))
           #'(define name
               (lambda formals
                 (let ((resume #f) (return #f))
                   (define yield
                     (lambda args
                       (call/cc
                        (lambda (cont)
                          (set! resume cont)
                          (apply return args)))))
                   (lambda ()
                     (call/cc
                      (lambda (cont)
                        (set! return cont)
                        (cond (resume (resume))
                              (else
                               (let () e0 e1 ...)
                               (unexpected-return 'name)))))))))))
        ((stx (name . formals) e0 e1 ...)
         #'(stx name (lambda formals e0 e1 ...))))))

  ) ;[end]

なかなかコンパクトになりました。これもcall/ccのおかげですね :)
ちょっと試してみましょう :D

Ypsilon 0.9.6-trunk/r248 Copyright (c) 2008 Y.Fujita, LittleWing Company Limited.
> (*1)
> (import (tidbits generator))
> (define-generator generate-12
    (lambda ()
      (yield 1)
      (yield 2)))
> (define sequence-12 (generate-12))
> (sequence-12)
1
> (sequence-12)
2
> (sequence-12)
error in iteration: unexpected return of generator function generate-12

(*1) svn trunkをインストールしていない場合には、ここで上のリストをREPLにコピペしてください。(tidbits generator)ライブラリがインポートできるようになります。
ここでgenerate-12がgenerator function、sequence-12がgenerator objectに相当するものとなります。(sequence-12)はPythonのsequence-12.next()に相当します。

generator functionは初期値などに使用するための引数を取ることができます。またyieldは通常の手続きですので以下のような使い方が可能です。

> (define-generator generate-iota
    (lambda (count from)
      (for-each yield (iota count from))))
> (define sequence-iota (generate-iota 5 -10))
> (sequence-iota)
-10
> (sequence-iota)
-9
> (sequence-iota)
...
> (define-generator generate-mv
    (lambda (count from)
      (yield 1 2)
      (yield 3 4)
      (apply yield (iota count from))))
> (define sequence-mv (generate-mv 4 5))
> (sequence-mv)
#<values 1 2>
> (let-values (((a . b) (sequence-mv))) (cons a b))
(3 4)
> (let-values (((a . b) (sequence-mv))) (cons a b))
(5 6 7 8)

http://linuxgazette.net/100/pramode.html にある例をSchemeに書き換えて実行してみます。

(define-generator pi
  (lambda ()
    (let loop ((deno 1.0) (nume 4.0) (value 0.0))
      (let ((new (+ value (/ nume deno))))
        (yield new)
        (loop (+ deno 2.0) (- nume) new)))))

(define pi-seq (pi))

(list (pi-seq) (pi-seq) (pi-seq))
;=> (4.0 2.666666666666667 3.466666666666667)

(define-generator euler-transform ; euler accelerator
  (lambda (seq)
    (define (square x) (* x x))
    (define s0 (seq))
    (define s1 (seq))
    (define s2 (seq))
    (let loop ((s0 s0) (s1 s1) (s2 s2))
      (yield (- s2 (/ (square (- s2 s1)) (+ s0 (- (* 2 s1)) s2))))
      (loop s1 s2 (seq)))))

(define pi-seq/euler (euler-transform (pi)))

(list (pi-seq/euler) (pi-seq/euler) (pi-seq/euler))
;=> (3.166666666666667 3.1333333333333337 3.1452380952380956)

generator functionのeuler-transformはgenerator objectを引数に取っていることに注意してください。これはeuler-transformがgeneratorを呼び出すgeneratorという汎用の部品となっているということです。これは便利なものですね!今まで知りませんでしたよ :p