初めに
Rubyで繰り返し処理をする時に「each」や「map(collect)」といったメソッドをよく使いますよね。
これらのメソッドをリファレンスマニュアルで参照していた時、
「each」も「map」もブロックを省略して呼び出すと「Enumerator(オブジェクト)を返します」と書かれているのを見て、Enumeratorって何?となり、色々調べ、頑張って理解してみたので、その備忘録としてここにまとめます。
単語としてのEnumeratorの意味
Enumerator
(意味)計数者・数え上げる人・列挙子・列挙型
(読み方)injúːmərèitər | injúː- (イニューメレイター)
この単語には、何かを数え上げたり、調査したものを整理したりする人という意味があるそうです。
プログラミングにおいては「列挙型」という日本語訳になります。
日本語にするとわかりやすく感じますね。
列挙
(意味)並べあげること。ひとつひとつ数え上げる。
RubyにおけるEnumeratorは「ひとつひとつ数え上げる役割を持つもの」なのかなぁ〜?と予想できます。
Enumeratorのリファレンスマニュアルを見てみる
以下リファレンスマニュアルより
要約
each 以外のメソッドにも Enumerable の機能を提供するためのラッパークラスです。また、外部イテレータとしても使えます。
Enumerable モジュールは、 Module#include 先のクラスが持つ each メソッドを元に様々なメソッドを提供します。
例えば Array#map は Array#each の繰り返しを元にして定義されます。
Enumerator を介することにより String#each_byte のような異なる名前のイテレータについても each と同様に Enumerable の機能を利用できます。
Enumerator を生成するには Enumerator.newあるいは Object#to_enum, Object#enum_for を利用します。
また、一部のイテレータはブロックを渡さずに呼び出すと繰り返しを実行する代わりに enumerator を生成して返します。
日本語訳見て、理解できそう!と思っていたのも束の間、
急に難しくなりました。笑
Enumerable。なんか似たような単語が出てきましたね。
Enumerableって?
以下リファレンスマニュアルより
要約
繰り返しを行なうクラスのための Mix-in。
このモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。
Array, Hash, Range, Enumerator等のクラスで、 Enumerableモジュールはインクルードされています。
ただし、効率化のため、そのクラスでEnumerableと同名・同等の機能を再定義(オーバーライド)しているケースも少なくなく、特にArrayクラスでは同名のメソッドを再定義していることが多いです。
RubyにはEnumerableモジュールというのがあり、繰り返し処理を行うための便利な機能(map, select, findなど)がここに定義されている。
このモジュールをクラスにインクルードして、正しく機能させるためには、インクルード先のクラスの中でeachを定義する必要がある。
といった感じでしょうか。
Arrayクラスのインスタンスに対してmapやselectなどが使えるのは、Arrayクラスの定義の中でeachを定義しているからだったのか!とここでわかります。
#Array(配列)はeachを持っているのでmapが使える。
[1, 2, 3].map { |x| x * 2 }
# => [2, 4, 6]
Enumerableについてわかったところで、再びEnumeratorの要約を見てみましょう。
要約
each 以外のメソッドにも Enumerable の機能を提供するためのラッパークラスです。また、外部イテレータとしても使えます。
Enumerable モジュールは、 Module#include 先のクラスが持つ each メソッドを元に様々なメソッドを提供します。
例えば Array#map は Array#each の繰り返しを元にして定義されます。
Enumerator を介することにより String#each_byte のような異なる名前のイテレータについても each と同様に Enumerable の機能を利用できます。
Enumerator を生成するには Enumerator.newあるいは Object#to_enum, Object#enum_for を利用します。
また、一部のイテレータはブロックを渡さずに呼び出すと繰り返しを実行する代わりに enumerator を生成して返します。
さっきよりちょっと理解しやすくなりました!
本来ならeachを定義しているクラスでないとmapやselectといった繰り返し処理用の便利なメソッドを使えない。
String#each_byteのような、eachとは違うけど繰り返しっぽいメソッドでもeachと同じようにEnumerabeleの提供する機能(mapやselect)が使えるのは、Enumeratorを介しているからだよ〜ってことのようです。
Enumeratorを介すって?
また新たな疑問が出てしまいました。
Enumeratorを介すってどういうことなのでしょう?
ここで、先ほどEnumeratorを介しているメソッドとして紹介されていたString#each_byteを見てみます。
以下リファレンスマニュアルより
instance method String#each_byte
each_byte {|byte| ... } -> self
each_byte -> Enumerator ←ここ!
文字列の各バイトに対して繰り返します。
each_byteをブロックなしで呼びだした場合、戻り値としてEnumeratorクラスのインスタンスが生成されています。
e = "abc".each_byte #ここで変数eはEnumeratorクラスのインスタンスになる
e.map { |byte| byte + 1 } #だからeにmapメソッドが使える!
# => [98, 99, 100]
Enumeratorを介してEnumerabeleの提供する機能(mapやselect)が使えるようになっていますね。
「each」「map(collect)」で出てきたEnumeratorを改めて見てみる
以下リファレンスマニュアルより
instance method Array#each
each {|item| .... } -> self
each -> Enumerator ←ここ!
instance method Array#collect
collect -> Enumerator ←ここ!
map -> Enumerator ←ここ!
先ほど見たString#each_byteと同じく、ブロックなしで呼び出しをするとEnumratorクラスのインスタンスが生成されるようになっています。
最初にeachやmapのリファレンスマニュアルで「Enumerator(オブジェクト)を返します」と書かれているのを見て抱いた、Enumeratorって何?という疑問が無事解決しました!
まとめ
-
Enumeratorを介すことでEnumerableモジュールの提供する繰り返し処理用の便利なメソッド(mapやcollect)が使えるようになる。
-
eachやmapやeach_byteなどのメソッドはブロックなしで呼び出すとEnumeratorを返すようになっている。
-
Enumeratorを介すことで、each以外の繰り返し系のメソッド(each_byteなど)にもmapやselectが使えるようになる。
最初はややこしく感じたEnumeratorですが、順を追ってリファレンスマニュアルを辿ることでなんとか理解することができました。