この前 イテレータ について書きましたが、 適当な道具さえ使えば、内部イテレータを外部イテレータ化することが可能です。 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は結構便利なのでした。