普通のイテレーション
[1,2,3].each do |i|
p i
end
みんな知っている
じゃあこれは?
x = [1,2,3].each
x
は何でしょう?
答え
x = [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
Enumeratorでした。
Enumerator??
(引用)
"each 以外のメソッドにも Enumerable の機能を提供するためのラッパークラスです。 また、外部イテレータとしても使えます。"
今回は後者
外部イテレータの利用例
iter = [1,2,3].each
begin
print iter.next() while true
rescue StopIteration
puts "iteration reached the end"
end
出力
1 2 3 iteration reached the end
nextメソッド??
Enumerator#next
- 現在までの列挙状態に応じて「次」のオブジェクトを返し、列挙状態を1つ分進める。
- 列挙が既に最後へ到達している場合は、
StopIteration
例外を発生する。
ref: https://docs.ruby-lang.org/ja/latest/class/Enumerator.html#I_NEXT
内部イテレータとの比較
内部イテレータ
[1,2,3].each {|i| p i }
外部イテレータ
it = [1,2,3].each
loop { p it.next } # Kernel.loopはStopIterationを捕捉する
- ユーザーが繰り返しを明示的に制御
(参考) C++やJavaは外部イテレータ
for( auto it = v.begin(); it != v.end(); ++it ) {
std::cout << *it << std::endl;
}
言語のclosureのサポートが弱いため
外部イテレータのメリットは?
あんまりない。基本、内部イテレータ使っておけばいい。
複数のコレクションに対して並列に処理したい場合には有用なケースもある
例
require 'prime'
a = (1..100).each
b = %w(first second third).each
c = []
loop do
i = a.next
c << i
c << b.next if Prime.prime?(i)
end
c
# => [1, 2, "first", 3, "second", 4, 5, "third", 6, 7]
実装はどうなっている?
Fiberを使って実装されている
Rubiniusの実装を見てみる
https://github.com/rubinius/rubinius/blob/master/core/enumerator.rb
def next
reset unless @fiber
val = @fiber.resume
raise StopIteration, "iteration has ended" if @done
return val
end
...
def reset
@done = false
@fiber = Fiber.new stack_size: STACK_SIZE do
obj = @object
@result = obj.each { |*val| Fiber.yield *val }
@done = true
end
end
Fiberとは?
"ノンプリエンプティブな軽量スレッド(以下ファイバーと呼ぶ)を提供します。 他の言語では coroutine あるいは semicoroutine と呼ばれることもあります。"
実行中にコンテキストを切り替えることができる
Fiberの例
f = Fiber.new do
puts "a"
Fiber.yield
puts "c"
end
f.resume
puts "b"
f.resume
puts "d"
#=> a b c d と出力される
外部イテレータの実装(再掲)
def next
reset unless @fiber
val = @fiber.resume
raise StopIteration, "iteration has ended" if @done
return val
end
...
def reset
@done = false
@fiber = Fiber.new stack_size: STACK_SIZE do
obj = @object
@result = obj.each { |*val| Fiber.yield *val }
@done = true
end
end
ちなみにPythonでは
内部イテレータの中でも実は外部イテレータが呼ばれている。
(Rubyでは外部イテレータの実装の中で内部イテレータが呼ばれている)
forでループを回すと
for i in my_container:
print(i)
中ではこんなことが行われる
- まず
my_container.__iter__()
が呼ばれて、iteratorが返る - iteratorに対して
__next__
が呼ばれる- 例外
StopIteration
が起きるまで繰り返す
- 例外
nextのoverride
class MyCollection:
def __init__(self, l):
self.list = l
def __iter__(self):
self.idx = 0
return self
def __next__(self):
print("next is called") # override
if self.idx >= len(self.list): raise StopIteration
a = self.list[self.idx]
self.idx += 1
return a
c = MyCollection([1,2])
for a in c: print(a)
# =>
# next is called
# 1
# next is called
# 2
# next is called
ループの挙動が変わった。
Rubyの場合はnextをオーバーライドしても内部イテレータの挙動は変わらない。
RubyとPythonの比較
- Rubyでは内部イテレータが基本
- 外部iteratorはeach+Fiberで実現
- coroutineを作りたい時にはFiberを使う
- Pythonでは外部イテレータが基本
- forループの中では実は外部イテレータが使われている
- coroutineを作りたい時には、Generator(RubyでいうところのEnumerator)を使って実現する
ちなみに rb_call では
for book in Book.where({"author":"foo"}):
... # ActiveRecordのcollectionに対するループ
-
__iter__
が呼ばれたら、対応するRubyのオブジェクトのeach
を呼ぶ -
__next__
が呼ばれたら、対応するRubyのオブジェクトのnext
を呼ぶ - Rubyで
StopIteration
が投げられたら、PythonでStopIteration
を投げる
まとめ
- rubyでは内部イテレータが基本
- 複数のコレクションを並列にイテレートしたい場合に外部イテレータが有用な箇所もある
- Rubyでは外部イテレータは「内部イテレータ+Fiber」を使って実装されている