LoginSignup
14
5

More than 5 years have passed since last update.

ちょっとマイナーなeachの使い方

Posted at
1 / 23

普通のイテレーション

[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」を使って実装されている
14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5