Rubyでその場で列挙子(Enumerator)を作成する方法
Rubyでは、コレクションを列挙子(Enumerator)として扱うことで、#map や #reduce といったお気に入りのメソッドを、余計なコードを一切書かずに利用できます。これがかなり便利なんです。
昔ながらのやり方では、列挙子を定義するのが少々面倒でした。新しいクラスを作り、Enumerable モジュールを include し、さらに #each メソッドを定義する必要があったのです。
ところがRuby 1.9以降、もっと軽量な方法でその場(オンザフライ)で列挙子を定義できるようになりました。早速見ていきましょう。
Enumeratorクラスのご紹介
Enumerator クラスを使うと、ブロック構文だけで使い捨ての列挙子を簡単に定義できます。次の例では、無限に乱数を生成し続ける列挙子を作成しています。
e = Enumerator.new do |y|
loop do
y << rand(10) # << 演算子は値を「yield」する
end
end
# 列挙子に10個の値を「yield」させてから停止させる
puts e.first(10).inspect # => [6, 6, 7, 2, 2, 9, 6, 8, 2, 1]
<< 演算子を奇妙な使い方をしていることに気づいたかもしれませんね。これは y.yield メソッドのショートカットです。列挙子が生成する各要素に対してこの呼び出しを行います。少し魔法のように感じるかもしれませんが、心配ご無用。実際、魔法のような仕組みなのです。
ポイントは、この列挙子が遅延評価(lazy)であることです。loop で無限ループを書いていても、first(10) のように必要な分だけ値を取り出せば、そこで処理が止まります。無限リストを安全に扱えるのは、この仕組みのおかげです。
コレクションのサイズを扱う
コレクションのサイズを求めるのは、遅延列挙子にとって悩ましい問題です。要素数を数えるにはコレクション全体を読み込む必要がありますが、それでは遅延列挙子を使う意味がなくなってしまいます。
とはいえ、回避策は一応あります。Enumeratorを作成する時点でコレクションのサイズが分かっているのであれば、コンストラクタに渡してしまえばよいのです。
# サイズが分かっている場合は、引数として長さを渡せる
e = Enumerator.new(10) do |y|
10.times { y << rand }
end
こうしておくと、e.size を呼び出したときに、コレクションを実際に走査することなく即座に 10 が返されます。
実践での活用例
ちょうど昨日、Honeybadgerの新しいドキュメントサイトに取り組んでいました。このサイトはJekyllで構築されており、私はドキュメント内の <h2> タグと <h3> タグをもとに目次を自動生成するプラグインを書いていました。
ここで厄介なのが、「ある <h3> タグがどの <h2> セクションに属するのか」を判定する処理です。nokogiriでHTMLをパースし、結果のドキュメントを順番にスキャンしていく必要があります。そこで私は、このロジックを抽象化してEnumeratorにしてみました。コードはこんな感じです。
def subheadings(el)
Enumerator.new do |y|
next_el = el.next_sibling
while next_el && next_el.name != "h2"
if next_el.name == "h3"
y << next_el
end
next_el = next_el.next_sibling
end
end
end
このメソッドは、与えられた <h2> 要素の直後にある兄弟ノードを順にたどり、次の <h2> が現れるまでの間に見つかったすべての <h3> 要素をyieldしていきます。
そして何より嬉しいのは、戻り値がEnumeratorなので、そのまま map や each といった便利なメソッドにつなげられる点です。
subheadings(h2_element).map { |el| el.text.strip }
# => ["はじめに", "セットアップ", "トラブルシューティング"]
HTMLの走査のような手続き的な処理も、Enumeratorで包むことで宣言的で再利用しやすいコードに生まれ変わります。ぜひ皆さんも、繰り返し処理が必要になった場面で試してみてください。
-
Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説
Rubyの内部構造をちょっと覗いてみませんか? それなら、この記事はきっとお役に立ちます。 なぜなら… この記事では、Rubyオブジェクトがメモリ上でどのように配置されているのか、そして内部データ構造を操作してクールなことを実現する方法を、一緒に探検していきます。 シートベルトを締めて、Rubyインタプリタの深淵への旅に出かけましょう! 配列(Array)のメモリレイアウト 配列を作成すると、Rubyはそのデータを保持するためにシステムメモリと、少しのメタデータを確保します。 メタデータには以下が含まれます: 配列のサイズ(要素数) 配列の容量(capacity) クラス情報 オブジェクトの
-
Rubyのcaseステートメントの多彩な活用法と仕組みを徹底解説
Rubyでif / elsifを使おうとしている場面では、代わりにcaseステートメントを使うことを検討してみてください。この記事では、caseステートメントのさまざまな活用例と、その内部で実際にどのような仕組みで動作しているのかを解説します。 補足:他のプログラミング言語では、これはswitch文として知られています。 Rubyにおけるcaseステートメントの構成要素は以下の通りです。 キーワード 説明 case caseステートメントの定義を開始します。処理対象となる変数を受け取ります。 when マッチ可能な各条件が、1つのwhen句に相当します。 else どの