Rubyの魔法を解き明かす:Enumerableモジュールの仕組みと使い方
Ruby Magicの新しいエピソードへようこそ!今回は、Rubyが最も「魔法」と呼ぶにふさわしい機能の一つ、Enumerableモジュールについて掘り下げます。このモジュールは、ArrayやHash、Rangeといった列挙可能なクラスで日常的に使うメソッドの大半を提供しています。この記事を通じて、列挙可能なオブジェクトで何ができるのか、列挙の仕組みはどうなっているのか、そしてたった一つのメソッドを実装するだけで独自クラスを列挙可能にする方法まで学べます。
Enumerable、#each、そしてEnumerator
「列挙(enumeration)」とは、オブジェクトを順番にたどっていくことを指します。Rubyでは、アイテムの集合と、その各要素をループ処理するためのメソッドを持つオブジェクトを「列挙可能(enumerable)」と呼びます。
組み込みの列挙可能クラスは、Enumerableモジュールをincludeすることで列挙機能を手に入れています。このモジュールには、#include?、#count、#map、#select、#uniqなど、便利なメソッドが多数用意されています。実は、配列やハッシュで使っているメソッドの多くは、これらのクラス自体には実装されておらず、Enumerableモジュールから取り込まれたものなのです。
補足: 一部のメソッド、例えばArrayクラスの#countや#takeは、Enumerableモジュールのものを使わず、配列専用に実装されています。これは通常、処理速度を向上させるためです。
Enumerableモジュールは、#eachという名前のメソッドに依存しています。このモジュールをincludeするすべてのクラスは、#eachを実装する必要があります。ブロック付きで配列に対して#eachを呼び出すと、配列の各要素に対してブロックが実行されます。
irb> [1,2,3].each { |i| puts "* #{i}" }
* 1
* 2
* 3
=> [1,2,3]
一方、#eachをブロックなしで呼び出すと、Enumeratorのインスタンスが返されます。
irb> [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
Enumeratorのインスタンスは、あるオブジェクトをどう反復処理するかを表します。Enumeratorは、オブジェクトの手動での反復処理や、列挙メソッドのチェーン(連結)を可能にします。
irb> %w(dog cat mouse).each.with_index { |a, i| puts "#{a} is at position #{i}" }
dog is at position 0
cat is at position 1
mouse is at position 2
=> ["dog", "cat", "mouse"]
#with_indexメソッドは、Enumeratorの動作を示す良い例です。まず配列に対して#eachが呼ばれ、Enumeratorが返されます。次に#with_indexが呼ばれ、配列の各要素にインデックスを付与することで、各要素の位置を出力できるようになります。
オブジェクトを列挙可能にする
内部的には、#max、#map、#takeなどのメソッドは、すべて#eachメソッドに依存して動作しています。
def max
max = nil
each do |item|
if !max || item > max
max = item
end
end
max
end
実際のEnumerableのメソッドはC言語で実装されていますが、上記のコードは#maxのおおよその動きを示しています。#eachですべての値をループしながら最大値を記憶しておくことで、最終的な最大値を返します。
def map(&block)
new_list = []
each do |item|
new_list << block.call(item)
end
new_list
end
#map関数は、各要素に対して渡されたブロックを呼び出し、その結果を新しいリストに格納します。すべての値をループし終えた後、そのリストを返します。
Enumerableのすべてのメソッドは何らかの形で#eachを使用しているため、独自クラスを列挙可能にする第一歩は、#eachメソッドを実装することです。
#eachの実装
クラスに#eachを実装し、Enumerableモジュールをincludeすれば、そのクラスは列挙可能になり、#min、#take、#injectなどのメソッドが無料で使えるようになります。
多くの場合、既存のオブジェクト(例えば配列)に処理を委譲して#eachを呼び出すだけで済みますが、ここではゼロから書く必要がある例を見てみましょう。この例では、連結リスト(linked list)に#eachを実装して、列挙可能にします。
連結リスト:配列を使わないリスト
連結リストとは、データ要素の集合であり、各要素が次の要素を指し示すデータ構造です。リスト内の各要素は「head(先頭)」と「tail(末尾)」という2つの値を持ちます。headはその要素自身の値を保持し、tailはリストの残り部分へのリンクです。
[42, [12, [73, nil]]]
3つの値(42、12、73)を持つ連結リストの場合、最初の要素のheadは42で、tailは2番目の要素へのリンクです。2番目の要素のheadは12で、tailは3番目の要素を保持します。3番目の要素のheadは73で、tailはnil、つまりリストの終端を表します。
Rubyでは、@headと@tailという2つのインスタンス変数を持つクラスを作ることで、連結リストを実現できます。
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
end
#<<メソッドはリストに新しい値を追加するために使います。渡された値をheadとし、元のリストをtailとする新しいリストを返すことで実現しています。
また、この例では#inspectメソッドを追加しています。これにより、リストの中身を確認できるようになります。
irb> LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
連結リストができたので、次は#eachを実装しましょう。#each関数はブロックを受け取り、オブジェクトの各値に対してそれを実行します。連結リストに実装する場合は、リストの再帰的な性質を利用できます。つまり、渡されたブロックをリストの@headに対して呼び出し、@tailが存在する場合は@tailに対して#eachを呼び出せばよいのです。
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
連結リストのインスタンスに対して#eachを呼び出すと、現在の@headを引数としてブロックが呼ばれます。その後、tailがnilでない限り、@tail内の連結リストに対してeachが呼び続けられます。
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each { |item| puts item }
42
12
73
=> nil
これで連結リストが#eachに応答するようになったので、include Enumerableしてリストを列挙可能にできます。
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.count
=> 3
irb> list.max
=> 73
irb> list.map { |item| item * item }
=> [1764, 144, 5329]
irb> list.select(&:even?)
=> [42, 12]
Enumeratorインスタンスを返す
これで連結リストのすべての値をループできるようになりましたが、まだ列挙メソッドをチェーン(連結)できません。それを実現するには、#each関数がブロックなしで呼び出されたときに、Enumeratorインスタンスを返す必要があります。
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
if block_given?
block.call(@head)
@tail.each(&block) if @tail
else
to_enum(:each)
end
end
end
オブジェクトをEnumeratorでラップするには、#to_enumメソッドを呼び出します。引数として:eachを渡します。これは、Enumeratorが内部で使用すべきメソッド名です。
これで、ブロックなしで#eachメソッドを呼び出せるようになり、列挙メソッドのチェーンも可能になりました。
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each
=> #<Enumerator: [42, [12, [73, nil]]]:each>
irb> list.map.with_index.to_h
=> {42=>0, 12=>1, 73=>2}
わずか9行のコードと1つのinclude
Enumerableモジュールを使って#eachを実装し、独自のEnumeratorオブジェクトを返すことで、わずか9行のコードと1つのincludeだけで、連結リストを強力なものにできました。
以上が、Rubyにおける列挙可能オブジェクトの概要でした。この記事への感想や質問があれば、ぜひお聞かせください。私たちは常に調査・解説すべきテーマを探しています。Rubyの「魔法」について読みたいテーマがあれば、遠慮なく@AppSignalまで教えてください!
-
Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説
Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端
-
RubyのEnumerableモジュール入門ガイド〜筆者イチオシのeach_consメソッドを徹底解説〜
Enumerableモジュールとは? Enumerableは繰り返し処理(イテレーション)用メソッドのコレクションで構成されるRubyのモジュールであり、Rubyが多くの開発者に愛される大きな理由のひとつとなっています。 Enumerableには、次のような便利なメソッドが含まれています: map select inject Enumerableのメソッドは、ブロックを渡すことで動作します。 ブロックの中には、各要素に対して実行したい処理を記述します。 例えば: [1,2,3].map { |n| n * 2 } このコードを実行すると、すべての数値が2倍になった新しい配列が得られます