こんにちは、hachi8833です。
今回は、#each
にブロックを渡して変数に代入する場合と、ブロックを渡さずに変数に代入する場合の挙動の違いについてまとめました。
記事後半ではmorimorihogeさんが#each
の挙動を深掘りしてくれました。
#each
の挙動の違い
1. #each
にブロックを渡す場合
#each
に{|str| puts "#{str} hohoho"}
というブロックを渡してa
に代入してみます。
arr = %w(hoge huga piyo foo baa)
a = arr.each {|str| puts "#{str} hohoho"}
pp a #=> ["hoge", "huga", "piyo", "foo", "baa"]
a
にはarr
のArrayオブジェクトがそのまま入ります。ArrayにはEnumerator#next
はないので、続けて以下を実行するとエラーになります。
a.next #=> エラー
2. #each
にブロックを渡さない場合
今度は#each
にブロックを渡さずにa
に代入してみます。
arr = %w(hoge huga piyo foo baa)
a = arr.each
pp a #=> #<Enumerator: ...>
a
にはEnumeratorオブジェクトが入りますので、#next
で順に取り出せます。
a.next #=> "hoge"
a.next #=> "huga"
a.next #=> "piyo"
a.next #=> "foo"
a.next #=> "bar"
a.next #=> エラー(終端に到達)
内部イテレータと外部イテレータ
morimorihogeさんとkazzさんによると、ブロックなしの#each
は外部イテレータとしてのEnumeratorオブジェクトを返すそうです。
逆に#each
にブロックを渡すと(この使い方がほとんどだと思います)、内部イテレータから渡された値をブロック変数(ブロック内の| |
で囲まれた変数)としてループを回せます。
- ブロックあり
#each
- 内部イテレータを利用してループする
- ブロックなし
#each
- 外部イテレータを返す
以下のリファレンスマニュアルで言う「一部のイテレータ」のひとつが#each
ですね。
Enumerator を生成するには Enumerator.newあるいは Object#to_enum, Object#enum_for を利用します。また、一部のイテレータはブロックを渡さずに呼び出すと繰り返しを実行する代わりに enumerator を生成して返します。
Ruby 2.4.0 リファレンスマニュアル: Enumeratorクラスより
内部イテレータを使うブロックあり#each
は、以下のようにイテレータの戻り値をそのまま返します。実際、1.ではa
にarr
の配列がそのまま代入されていましたね。
ブロック付きで呼び出された場合は、 生成時に指定したイテレータの戻り値をそのまま返します。
Ruby 2.4.0 リファレンスマニュアル: Enumerator#eachより
なお、結果を取り出したい場合はmap
などを使います。
pry(main)> a = arr.map {|str| str * 2}
#=> a は ["hogehoge", "hugahuga", "piyopiyo", "foofoo", "baabaa"]になる
追伸: Enumerator#each
の仕様について
ruby-doc.orgのEnumeratorによると、Enumeratorのほとんどのメソッドはブロックなしの場合にはイテレーションをラップするEnumeratorを返すとのことです。
Most methods have two forms: a block form where the contents are evaluated for each item in the enumeration, and a non-block form which returns a new Enumerator wrapping the iteration.
ruby-doc.orgのEnumeratorより
ブロックなしの#each
の動作について明確に規定されているとまではいきませんが、多くの場合はEnumeratorを返すようです。
参考: matzによる内部イテレータと外部イテレータの解説
まつもと直伝 プログラミングのオキテ 第5回(2)に、matz直々の解説がありますので、一部を引用します。2005年の記事なのでご了承ください。
内部イテレータは余分なクラスを作らず,使うのも作るのも簡単です。しかし,言語がクロージャをサポートしていないとループ本体と外側とで情報を共有するために工夫が必要になり,C言語の例で見たようにループとしての使い勝手が悪くなります。このため,クロージャを持たないC++やJavaでは外部イテレータが採用されているのです。
一方,外部イテレータ方式では,コンテナとイテレータはお互いに密接な関係を持ちますから,作るのも難しく,利用するためのコード量も少々増えます。外部イテレータ方式を,一行で書けるeachメソッドを用いた繰り返しと比べてみると一目瞭然でしょう。しかし,外部イテレータ方式にも長所があります。複数のコンテナから一つずつ要素を取り出し,並行に処理するような手続きは外部イテレータでは簡単に書けますが,内部イテレータではそうはいきません。
「内部イテレータではクロージャが重要」なんですね。
Appendix: Enumerableが要求する#eachの詳細仕様はどうなっているのか?
※この節だけmorimorihogeが書いてます
ここまではArrayやHashのようなRuby組み込みクラスを想定して書かれた内容なのですが、自分たちでEnumerableなクラスを作成する場合、そもそも引数無し#each
が外部イテレータを返すように実装することは必須なのでしょうか?
Enumerableのドキュメントには以下のように書かれています。
このモジュールの メソッドは全て each を用いて定義されているので、インクルード するクラスには each が定義されていなければなりません。
docs.ruby-lang.org: Enumerableモジュールのドキュメントよりより
ただ、この書き方だけでは#each
の具体的な実装についてまでは言及されておらず、穿った見方をすれば
class HogeCollection
include Enumerable
def each
puts 'あばばばばばば'
end
end
みたいなコードでもいいんじゃないかという話にもなってしまいます(ちなみに実はエラーにならず動きます。意味があるかどうかは別としてw)。
一方、ruby-doc.orgの方のEnumerableドキュメントでは以下のようにもう少し細かい指定があります。
The class must provide a method each, which yields successive members of the collection.
ruby-doc.org: Enumerableモジュールのドキュメントより
#each
はcollectionのメンバに対して連続的にyieldしろ、的な指定ですね。これで少なくとも#eachはyieldしなければいけない(blockを受け取って処理する必要がある)ことがわかります。
しかし、ここで再掲となりますが、今回のタイトルにもある引数無し#eachについては指定がありません。
Most methods have two forms: a block form where the contents are evaluated for each item in the enumeration, and a non-block form which returns a new Enumerator wrapping the iteration.
ruby-doc.orgのEnumeratorより
「Most methods」というところがミソで、この書き方だとsome methodsは引数無し#eachを呼び出した際にEnumeratorが返ってこない可能性もあるということになります。
これだと、Enumerableを汎用的に受け取って外部イテレータを使った処理を書く時には
def oreore_method(collection)
iterator = collection.each
if iterator.kind_of? Enumerator
# 処理
else
raise 'collection.each does not return Enumerator'
end
end
みたいな例外をいちいち記述しないといけないのかな?とも取れます。結論から言うとこれはYESです。Enumerableなクラスから外部イテレータを取り出したいと思ったときに#each
を使ってしまうと予期しない結果が起こる可能性があります。
しかし、とはいえEnumerableから確実に外部イテレータを取得したい、ということもあるかもしれません。そんなときは#collect
、#map
を使うと引数なし#eachがEnumeratorを返さない実装になっていてもEnumeratorを取り出すことができます。
具体的に見てみましょう。
class HogeCollection
include Enumerable
def initialize
@members = [1,2,3]
end
def each(&block)
if block_given?
@members.each{ |obj| yield obj }
else
puts 'no block given.'
end
end
end
とした上でPryで動かしてみます。この場合、
[2] pry(main)> hoge = HogeCollection.new
=> #<HogeCollection:0x007ff3b1929e90 @members=[1, 2, 3]>
[3] pry(main)> hoge.each
no block given.
=> nil
と#eachは確かにnilを返してしまうのですが、
[4] pry(main)> hoge.map
=> #<Enumerator: ...>
と#mapするとEnumeratorを取得することができます。もちろんこのEnumeratorは普通に#nextできて、
[7] pry(main)> e.next
=> 1
[8] pry(main)> e.next
=> 2
[9] pry(main)> e.next
=> 3
[10] pry(main)> e.next
StopIteration: iteration reached an end
from (pry):23:in `next'
と期待した動作をしてくれます。つまり、Enumerableモジュールは#each
がブロックに対してyieldすること以上は要求していないということになります。
整理すると以下の通りになります。
#each
にblockを渡した場合に要素がyieldされることはEnumerableモジュールのinclude条件に規定されている#each
にblockを渡さない場合にEnumeratorが返るかどうかは規定されていないが、慣例的には返却されることが多い- もし
#each
がEnumeratorを返さなくても、引数無し#map等のEnumeratorを明示的に返却するメソッドを使えばEnumeratorを取り出すことができる
公式ドキュメントの通りといえば通りなのですが、この辺りは誤解していると汎用的に書いたつもりが動かないケースを生み出してしまうので気をつけると良いと思います。