概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Stop including Enumerable, return Enumerator instead | Arkency Blog
- 原文公開日: 2014/01/08
- 著者: Robert Pankowecki
- サイト: Arkency Blog
画像は元サイトからの引用です。
Ruby: EnumerableをincludeするよりEnumeratorを返そう(翻訳)

自分のクラスにEnumerableモジュールをincludeする人をむやみやたらと見かけます。しかし私としては多くの場合、Enumerableで使えるeach_with_indexやらtake_whileやらminmaxといったメソッドを自分のクラスの中で使えるようにすることがそのクラスの責務の中心にあるとは考えにくいのです。
私ならそのような場合、Java方式でやるのが好みです。つまり、コレクションでEnumerableのさまざまな便利メソッドのどれかひとつを呼び出す必要のあるユーザー向けに、外部のEnumeratorを提供してあげるのです。ここで自問自答すべきは「そのクラスはコレクションなのか?」です。イエスなら、include Enumerableするのははまったくもって当然です。しかしクラスそのものがコレクションではなく、それ以外の何かを含む可能性のあるクラスだったり、コレクションを提供するクラスであるならば、外部のEnumeratorこそがおそらく解決策となることでしょう。
標準ライブラリ
定番中の定番であるArray#eachメソッドをブロックなしで呼び出すと、次のようにEnumeratorオブジェクトが1つ返されます。
e = [1,2,3].each
# => #<Enumerator: [1, 2, 3]:each>
新しい要素を手動でフェッチすることもできます。
e.next
# => 1
e.next
# => 2
e.next
#=> 3
e.next
# StopIteration: iteration reached an end
Enumeratorがかたじけなくも提供してくれるEnumerable系メソッドのひとつを使うこともできます。
e = [1,2,3].each
e.partition{|x| x % 2 == 0}
# => [[2], [1, 3]]
Enumeratorを作る
Enumeratorの作り方は3とおりあります(訳注: #to_enumとenum_forは少なくともRuby 2.5ではObjectのメソッドです)。
Kernel#to_enumKernel#enum_forEnumerator.new
しかしMRIの実装を覗いてみると、 #to_enumと#enum_forの実装が同じであることがわかります。
rb_define_method(rb_mKernel, "to_enum", obj_to_enum, -1);
rb_define_method(rb_mKernel, "enum_for", obj_to_enum, -1);
rb_define_method(rb_cLazy, "to_enum", lazy_to_enum, -1);
rb_define_method(rb_cLazy, "enum_for", lazy_to_enum, -1);
上のコードは以下で参照できます。
今度はrubyspecを覗いてみると、両者は完全に同一の振る舞いを期待されているので、本記事執筆時点では両者に違いはないと推測しています(訳注: 以下の2つがリンク切れだったため、翻訳時点のリンクに変更しています)。
すなわち、これらを使うコード例ではいつでも両者を交換可能です。
#to_enumと#enum_for
さて、#to_enumや#enum_forにはどんな使いみちがあるのでしょうか?どちらも、引数をyieldする任意のメソッドを元にEnumeratorを作成できます。#eachメソッドを元にEnumeratorを作成する手法はよく使われます(今更ですが)。
a = [1,2,3]
enumerator = a.to_enum(:each)
実際の使い方については後述します。
Enumerator.new
こちらの方法については、上述とは対照的にRuby docに詳しく書かれているので、そこからそのままコピペします(訳注: 原文はRuby 2.1ドキュメントへのリンクでしたが2.5に変更しました: 内容は同じです)。
渡されたブロックによって繰り返しが定義される。ブロックパラメータとして渡される「yielder」オブジェクトは値の
yieldに使える。
fib = Enumerator.new do |y|
a = b = 1
loop do
y << a
a, b = b, a + b
end
end
fib.take(10) # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
オプションパラメータを用いて、サイズをlazyに算出する方法を指定できる。値、または呼び出し可能オブジェクトのいずれかを使える。
以下は私のコード例です。
polish_postal_codes = Enumerator.new(100_000) do |y|
100_000.times do |number|
code = sprintf("%05d", number)
code[1] = code[1] + "-"
y.yield(code)
end
end
polish_postal_codes.size # => 100000
# どの要素も算出せずに返す
polish_postal_codes.take(3) # => ["00-000", "00-001", "00-002"]
Enumerator.newする理由
もちろん、(Arrayなどの)コレクションを返すとパフォーマンス上の理由で困難が生じたり実行不可能になるような場合は、Enumeratorを返すのが最も理にかなっています。IO#each_byteやIO#each_charなどがそうです。
注意すべき点は?
実際にはそれほどありません。自分のメソッドが値をyieldするのであれば、ブロックが渡されない場合には常に#to_enumでそのメソッド自身を元にEnumeratorを作成するだけにしておきましょう(上述のとおり#enum_forでも挙動は同一です)。ややこしそうに思えるかもしれませんがそんなことはありません。次のコード例をご覧ください。
require 'digest/md5'
class UsersWithGravatar
def each
return enum_for(:each) unless block_given? # ここで魔法が炸裂!!
User.find_each do |user|
hash = Digest::MD5.hexdigest(user.email)
image = "http://www.gravatar.com/avatar/#{hash}"
yield user unless Net::HTTP.get_response(URI.parse(image)).body == missing_avatar
end
end
private
def missing_avatar
@missing_avatar ||= begin
image_url = "http://www.gravatar.com/avatar/fake"
Net::HTTP.get_response(URI.parse(image_src)).body
end
end
end
私たちが手がけているSuper Startupは数百万ものユーザーを抱えており、うち数千人がgravatarを使っています。それらを一切合切1個のarrayに押し込めて返したくありませんよね?しかしご心配なく。return enum_for(:each) unless block_given?という魔法のようなワンライナーのおかげで、全データを計算せずにこのコレクションを共有できます。
これは本当に役に立つことがあります。特に、呼び出し元で一部しか必要とされていない場合に有用です。
class PutUsersWithAvatarsOnFrontPage
def users
@users ||= UsersWithGravatar.new.each.take(20)
end
end
呼び出し元でちょいとばかり#lazyな処理を行いたい場合にも便利です。
UsersWithGravatar.
new.
each.
lazy.
select{|user| FacebookFriends.new(user).has_more_than?(10) }.
and_what_not # ...
おや、ついlazyの話をしてしまいましたが、このくらいにしておきましょう。というのもlazyにはまったく別の良記事があるからです。
まとめ
Ruby標準ライブラリの振る舞いを一貫させるために、yieldする自作メソッドにブロックが渡されないときにはどうかEnumeratorを返してやってください。
return enum_for(:your_method_name_which_is_usually_each) unless block_given?`
たったこれだけで完了です。
自分で作るクラスにEnumerableが必要であるとは限りません。Enumeratorを返すだけで済むならそれに越したことはありません。
もっと知りたい方へ
本記事を気に入っていただけた方は、Arkencyのニュースレターの購読をお願いします。開発者を悪い意味で驚かせないRailsアプリを構築するための弊社の日々の取り組みのノウハウを届けします。
弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。
