龍虎氏の記事(2024/03/06)に対するコメント。
ジェネレータに関するScheme/Racketでの解釈は色々ある。
Pythonのジェネレータに関しての、典型例は例えば次のようなモノがある。
def generator(): # yield文によって正の整数を返す関数
n = 0
while True:
yield n
n += 1
使用例は次のようになる。
>>> gen = generator() # 正の整数を返すジェネレータを生成
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
このジェネレータは延々と1づつ増えた整数を返していく・・・・・・。
そう、一つの解釈としては、実はPythonのジェネレータはScheme/Racketで言うトコの遅延評価なんだ。
;; Racket/Schemeの場合> (require srfi/41)
> (define *gen* (stream-from 0))
> (stream-ref *gen* 0)
0
> (stream-ref *gen* 1)
1
> (stream-ref *gen* 2)
2
もっと正確に言うと、Pythonのジェネレータは遅延評価そのもの、と言うより遅延評価のダウングレード版だ。
遅延評価のデータ構造はそもそも永続的だが、Pythonのジェネレータはそうじゃない。破壊的に変更されていく。
つまり、元のPythonのコードはRacket/Schemeではある意味次のコードを意味してる。
(require srfi/41)
(define (generator)
(let ((strm (stream-from 0)))
(lambda ()
(begin0
(stream-car strm)
(set! strm (stream-cdr strm))))))
実行例は次のようになる。
> (define gen (generator))
> (gen)
0
> (gen)
1
> (gen)
2
>
かなりPythonの動作に近づいていってる。ある意味Racket/Schemeの持ってる「遅延評価」によるデータ構造(ストリーム)を破壊的変更によって制限を付けたクロージャを作る事によって実現してるわけだ。
もっとPythonに寄せると次のようにも書けるだろう。
(require srfi/41)
(define (generator)
(let ((strm (stream-from 0)))
(lambda (arg)
(case arg
((next) (begin0
(stream-car strm)
(set! strm (stream-cdr strm))))))))
Scheme/Racketによる「オブジェクト指向の仕組み」を思い出そう。この記述で関数generatorが生成するクロージャはnext、と言うメソッドを持つ。
> (define gen (generator))> (gen 'next)
0
> (gen 'next)
1
> (gen 'next)
2
PythonのイテレータやジェネレータはScheme/Racketの知識で解釈すると遅延評価、あるいは遅延評価をダウングレードしたモノ、って捉えた方が分かりやすいと思う。
Pythonで「無限」を記述する場合はイテレータやジェネレータに頼る。同じ構造をRacket/Schemeでは「ストリーム」と言う形式で記述出来、それはPythonのイテレータやジェネレータより遥かに強力で「正しい」機能なんだ。