やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

組込ライブラリ(Enumerator)

 Array並に大事。eachができるのはこいつのおかげ。

成果物

情報源

Enumerator

each 以外のメソッドにも Enumerable の機能を提供するためのラッパークラスです。また、外部イテレータとしても使えます。

 だいたいブロックを受け取れるメソッドは、ブロックがない場合、このEnumeratorを返す。かなり汎用的に使われているため大事。

Enumerable モジュールは、 Module#include 先のクラスが持つ each メソッドを元に様々なメソッドを提供します。例えば Array#map は Array#each の繰り返しを元にして定義されます。 Enumerator を介することにより String#each_byte のような異なる名前のイテレータについても each と同様に Enumerable の機能を利用できます。

 そんな感じでループするときは大体用いられる。

Enumerator を生成するには Enumerator.newあるいは Object#to_enum, Object#enum_for を利用します。また、一部のイテレータはブロックを渡さずに呼び出すと繰り返しを実行する代わりに enumerator を生成して返します。

 ですよね。

注意

外部イテレータとしての機能は Fiber を用いて実装されているため Fiber と同じ制限があります。例えば以下のようなスレッドをまたいだ呼び出しはエラーになります。

a = nil
Thread.new do
  a = [1, 2, 3].each
  a.next
end.join

p a.next
#=> t.rb:7:in `next': fiber called across threads (FiberError)
#      from t.rb:7:in `<main>'

 Fiberはコルーチンのことらしい。

ノンプリエンプティブな軽量スレッド(以下ファイバーと呼ぶ)を提供します。他の言語では coroutine あるいは semicoroutine と呼ばれることもあります。 Thread と違いユーザレベルスレッドとして実装されています。

 コルーチンについてググった。

コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。

メンバ抜粋

特異メソッド

new produce

インスタンスメソッド

+ each feed next next_values peek peek_values 
rewind size with_index with_object

new

 Enumratorオブジェクトを返す。

new(obj, method = :each, *args) -> Enumerator

オブジェクト obj について、 each の代わりに method という名前のメソッドを使って繰り返すオブジェクトを生成して返します。 args を指定すると、 method の呼び出し時に渡されます。

str = "xyz"
enum = Enumerator.new(str, :each_byte)
p enum.map {|b| '%02x' % b }   # => ["78", "79", "7a"]

 私の環境で実行したら以下エラーが出たんだが……。

tried to create Proc object without a block (ArgumentError)
new(size=nil) {|y| ... } -> Enumerator

Enumerator オブジェクトを生成して返します。与えられたブロックは Enumerator::Yielder オブジェクトを引数として実行されます。

 Enumerator::Yielderとかいう新たなものが出てきた。イテレータの引数がYielderってことかな?

生成された Enumerator オブジェクトに対して each を呼ぶと、この生成時に指定されたブロックを実行し、Yielder オブジェクトに対して << メソッドが呼ばれるたびに、 each に渡されたブロックが繰り返されます。

 はあ。文章だとよくわからない。

new に渡されたブロックが終了した時点で each の繰り返しが終わります。このときのブロックの返り値が each の返り値となります。

enum = Enumerator.new{|y|
  (1..10).each{|i|
    y << i if i % 5 == 0
  }
}
enum.each{|i| p i }

#=>  5
#   10


fib = Enumerator.new { |y|
  a = b = 1
  loop {
    y << a
    a, b = b, a + b
  }
}

p fib.take(10) #=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

produce

 与えられたブロックを呼び出し続ける、停止しない Enumerator を返す。

produce(initial = nil) { |prev| ... } -> Enumerator

ブロックの戻り値が、次にブロックを呼び出す時に引数として渡されます。 initial 引数が渡された場合、最初にブロックを呼び出す時にそれがブロック呼び出しの引数として渡されます。initial が渡されなかった場合は nil が渡されます。

ブロックが例外 StopIterationを投げた場合、繰り返しが終了します。

# 1, 2, 3, 4, ... と続く Enumerator
Enumerator.produce(1, &:succ)

# next を呼ぶたびランダムな数値を返す Enumerator
Enumerator.produce { rand(10) }

# ツリー構造の祖先ノードを列挙する Enumerator
ancestors = Enumerator.produce(node) { |prev| node = prev.parent or raise StopIteration }
enclosing_section = ancestors.find { |n| n.type == :section }

 succは次の要素を取得するものっぽい。nextと同義。1.succなら2が返る。そんな便利メソッド。

 以下みたいな感じで使う。

# 1, 2, 3, 4, ... と続く Enumerator
e = Enumerator.produce(1, &:succ)
p e
p e.next

# next を呼ぶたびランダムな数値を返す Enumerator
e = Enumerator.produce { rand(10) }
p e
p e.next

このメソッドは Enumerable の各メソッドと組み合わせて使うことで、 while や until ループのような処理を実装できます。例えば Enumerable#detect, Enumerable#slice_after, Enumerable#take_while などと合わせて使えるでしょう。

 なんか難しそう。

メソッド 概要
detect 要素に対してブロックを評価した値が真になった最初の要素を返す
slice_after パターンがマッチした要素、もしくはブロックが真を返した要素を末尾の要素としてチャンク化(グループ化)したものを繰り返すEnumeratorを返す
take_while Enumerable オブジェクトの要素を順に偽になるまでブロックで評価し、最初に偽になった要素の手前の要素までを配列として返す。

Enumerable のメソッドと組み合わせる例

# 次の火曜日を返す例
require "date"
Enumerator.produce(Date.today, &:succ).detect(&:tuesday?)

# シンプルなレキサーの例
require "strscan"
scanner = StringScanner.new("7+38/6")
PATTERN = %r{\d+|[-/+*]}
Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first
# => ["7", "+", "38", "/", "6"]

 StringScannerは部分文字列を取得する機能っぽい。

 どうでもいいけどブロックの後にメソッドチェーンが続くのはすごい違和感ある。

each

str = "Yet Another Ruby Hacker"
#enum = Enumerator.new(str, :scan, /\w+/)#=> tried to create Proc object without a block (ArgumentError)
#enum = Enumerator.new{|y| y << %w(Yet Another Ruby Hacker)}
enum = Enumerator.new{|y| 
  y << 'Yet' 
  y << 'Another' 
  y << 'Ruby' 
  y << 'Hacker'}
enum.each {|word| p word }              # => "Yet"
                                        #    "Another"
                                        #    "Ruby"
                                        #    "Hacker"
str.scan(/\w+/) {|word| p word }        # => "Yet"
                                        #    "Another"
                                        #    "Ruby"
                                        #    "Hacker"

 ドキュメントにかかれている方法でnewしたらエラーになるんですけど。こっちのほうが便利なんですけど。なんで使えなくなったの?

#enum = Enumerator.new(str, :scan, /\w+/)
#=> tried to create Proc object without a block (ArgumentError)

所感

 超便利そうだけど難しい。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux