概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 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_enum
Kernel#enum_for
Enumerator.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アプリを扱ってる方に有用です。