部分継続による Python 風ジェネレータ
ここ数日 HTML パーサーの開発に取り組んでいたんですが、その過程で次のような問題にぶつかりました。
HTML を構文解析して木構造を作る際に、トークン (字句) を一つずつ返してくれるジェネレータが欲しい、と思ったんです。
一般に、言語の解析は字句解析と構文解析の2段階から成りますが (参照: Wikipedia:構文解析) 字句を一つ取り出してその都度構文解析に回したい、と考えたわけです。
例えば、
<body> <p>text with <a href="foo.htm">anchor</a> in between</p> </body>
のような HTML 片があった時に、次のようにして一つずつのトークンを得たい、ということなんです:
(body) (p) "text with " (a (@ (href "foo.htm"))) "anchor" (/ a) " in between" (/ p) (/ body)
ここで重要なのは、"<" と ">" で囲まれた要素を読み込んだ後は "<" ">" を含まない要素を (もしあれば) 読む、という風に、読み込みたい順序が決まっていることです。
したがって、指定の順序で読み込みながらループを回し、トークンのリストを得ることは比較的容易でした:
(define (tokenize in) (let loop ((acc '())) (cond ((read-tag in) => (lambda (tag) (loop (append acc (cons tag (as-list (read-content in))))))) (else acc)))) (define (as-list x) (if x (list x) '()))
問題は、このように一気に字句解析して (長々とした) リスト・データを作ってしまうのではなく、ポートからトークンを読み出すごとに構文解析の側に提供したい、ということなんです。
「次に読み込むべきものは何か」を記憶しながら制御を呼び出し元に戻す方法、ということで考えた結果、部分継続を使う方法を思い付きました:
(define (generate-token in) (define next #f) (define (loop) (cond ((read-tag in) => (lambda (tag) (shift k (set! next k) tag) (let ((body (read-content in))) (when body (shift k2 (set! next k2) body)) (loop)))) (else #f))) (lambda () (if next (next #f) (reset (loop)))))
さらに、shift の部分を括り出すとスッキリします:
(define (generate-token in) (define next #f) (define (yield x) (shift k (set! next k) x)) (define (loop) (cond ((read-tag in) => (lambda (tag) (yield tag) (cond ((read-content in) => yield)) (loop))) (else #f))) ; StopIteration (lambda () (if next (next #f) (reset (loop)))))
yield を評価した時点で制御がジェネレータの呼び出し元に戻り、値が返されます。再びジェネレータに入った時は、前の yield の続きから処理が再開されます。
利用例:
(call/input-port url get-pure-port (lambda (in) (let ((next (generate-token in))) (let loop ((tok (next))) (when tok (display tok) (newline) (loop (next)))))))
ついでなのでマクロを書いてみると:
(define-syntax (defgen stx) (syntax-case stx () ((defgen (?fun ?args ...) ?body ...) (with-syntax ((yield (datum->syntax stx 'yield))) #'(define (?fun ?args ...) (define next #f) (define (yield x) (shift k (set! next k) x)) (lambda () (if next (next #f) (reset ?body ...))))))))
(defgen (generate-token in) (let loop () (and-let* ((tag (read-tag in))) (yield tag) (cond ((read-content in) => yield)) (loop))))
と書けます。
datum->syntax により yield というキーワードが定義の中で参照可能になっていますが、構文ではなく通常の関数として呼び出せる点が Python とは違うところでしょう。
普通に call/cc を使ってジェネレータを作ることもできるんですが (参照: Scheme:generatorとdoとwhile)、部分継続の方がフルな継続よりも軽量ですし、呼び出し元に戻るための継続を捉える必要が無い (shift を評価した時点で脱出が起こる) のでプログラミングも楽です。