この前 イテレータ について書きましたが、 適当な道具さえ使えば、内部イテレータを外部イテレータ化することが可能です。 Pythonのgeneratorは要するにcoroutineを使って外部イテレータ化しているわけで、 coroutineを使える環境であれば、同じ手法が使えます。 現実逃避をかねて、Rubyでたまには遊んでみます。
論より証拠で、まずはコードを。
class StopIteration < Exception end class Iterator def initialize(container, name, args) @container = container @name = name @args = args @inner = nil @outer = nil end def advance callcc do |o| @outer = o if @inner.nil? @container.__send__(@name, *@args) do |*i| callcc do |c| @inner = c @outer.call(*i) end end raise StopIteration else @inner.call end end end def iter self end end module Iterable def iter(name = :each, *args) return Iterator.new(self, name, args) end end
Rubyには Continuation があるので、 それを使ってます。 Continuationは道具としては少々プリミティブなので、 ちょっと面倒くさいことになってます。
Iteratorクラスは外部イテレータを表現するクラスです。 Pythonではnextで一つ進めて値を取り出しますが、 Rubyではnextが予約されてしまっているので、 代わりにadvance(C++から拝借)を使いました。
advance内では二つのContinuationが必要です。 外側用と内側用です。 外側の@outerは呼び出し元に帰るために必要なもので、 スタックに置くと、これも巻き戻されてしまうので、 インスタンス変数に突っ込みます。 内側の@innerは内部イテレータを途中で止めておくのに使います。 それ以上進められなくなったら、 例外クラスのStopIterationを投げます。
Iterableモジュールは所謂mix-inで、 既存のクラスを外部イテレータに対応させます。 iterメソッドを呼び出すと、Iteratorオブジェクトが返ります。 デフォルトでeachで繰り返すようにしました。
Arrayでテストしてみます。
class Array include Iterable end a = [1, 2, 3] i = a.iter while true begin p i.advance rescue StopIteration break end end i = a.iter(:each_with_index) while true begin p i.advance rescue StopIteration break end end
以下が出力。
1 2 3 [1, 0] [2, 1] [3, 2]
ちゃんと動いていることが分かります。
しかしこのままでは使い勝手が悪いので、 syntax sugarが必要です。 定番のeachをIteratorに対して使えるようにします。 これをIteratorクラスに突っ込んでやります。
def each while true begin yield advance rescue StopIteration break end end end
すると、
[1,2,3].iter.each {|e| p e }
は
1 2 3
となって、楽に書けることが分かります。
しかし、これではちっとも外部イテレータの利点が活かせてない、 というか、内部イテレータそのままです。 活用例として、 Pythonのitertools に入っているizipのようなメソッドを作ってみます。
class IZipContainer include Iterable def initialize(*iterables) @iterables = iterables.collect {|i| i.iter} end def each while true begin yield @iterables.collect {|i| i.advance} rescue StopIteration break end end end end def izip(*iterables) IZipContainer.new(*iterables) end
複数のIteratorオブジェクトに対してadvanceを繰り返すだけのIZipContainerクラスを作成し、 簡単に書いているような気がするためのizipメソッドを書きました。 すると、こんな風に書けます。
izip([1, 2], [3, 4]).each {|i,j| puts "#{i} #{j}" }
1 3 2 4
Rubyのzipとは異なり、前もって合体させた配列を作ったりはしないので、 無限列に対しても使用することができます。
典型的には、外部イテレータはcoroutineのように使用でき、 ステート・マシンなどを容易に記述することができるようになります。 例えば、
def each while true yield foo if @i > 0 yield bar else yield baz end end end
のように書き、外部イテレータ化してやると、 advanceする度に、
というのを永久に繰り返すオブジェクトが作れます。 ステート・マシンでフラグ等を使って書くことも出来ますが、 このような方針の方がより自然にプログラムを記述できることが多いと感じます。
というわけで、RubyのContinuationは結構便利なのでした。