2007-02-16

_ 内部イテレータを外部イテレータ化する

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

  1. fooを呼ぶ
  2. @iが0より大きいなら、barを呼ぶ
  3. そうでなければ、bazを呼ぶ

というのを永久に繰り返すオブジェクトが作れます。 ステート・マシンでフラグ等を使って書くことも出来ますが、 このような方針の方がより自然にプログラムを記述できることが多いと感じます。

というわけで、RubyのContinuationは結構便利なのでした。

[]

トップ «前の日記(2007-01-31) 最新 次の日記(2007-03-01)»