「Ruby のブロック付きメソッドとイテレータ - yield の様々な使い方」のつづき
1. 要素を保持する親クラスと、要素となる子クラス
- 「人」が「グループ」 に所属している。
- 「人」は `名前' と `年齢' を属性として持つ。
「グループ」クラスは親クラスで、「人」クラスは要素となる子クラス。
class Person attr_reader :name , :age def initialize(name,age) @name = name @age = age end end
親クラスとなる「グループ」は、保持する「人」の集合に対して責務を持っており、「人」を「グループ」に追加する操作を持つとする。
class Group def initialize @persons = [] end def add(person) @persons << person self end end
2. Enumerable モジュールを親クラスにインクルード
「グループ」クラスには、「人」を追加する操作だけではなく、
- 特定の「人」を抽出したり
- 全ての人が、ある条件を満たすのか調べる
ようなメソッドも定義したい。 Ruby では、自分でそのような操作を実装しなくても、「グループ」に Enumerable モジュールをインクルードするだけで、便利なメソッドがいくつも追加される。
Enumerable モジュールを「グループ」にインクルードする方法は、
- 「グループ」クラス内に、include Enumerable と記述。
- 「グループ」に、「人」の集合の要素を順番に取り出す each メソッドを定義。
このような Enumerable モジュールの使い方を Mixin と呼ぶ。
each メソッドを定義する理由は、Enumerable - Rubyリファレンスマニュアル によると、
(Enumerable クラスは) 繰り返しを行なうクラスのための
Mix-in
。このモジュールのメソッドは全てeach
を用いて定義されているので、インクルードするクラスにはeach
が定義されていなければなりません。
each メソッドの実装方法
each メソッドの実装、要素を保持する親クラスから、要素をひとつづつ取り出し、その要素にブロック(関数)を適用すること。以下に、Group クラスの each メソッドの実装を示す。
class Group include Enumerable def each @persons.each do |person| yield person end end end
each メソッドの中で、yield が使われている。この記述で、yield の呼び出しの際、「グループ」が保持している各々の person を、each メソッドに与えられるブロックに引き渡していることになる。
この書き方でわかりづらい点は、each メソッドの引数にブロックが与えられることが明示されていないこと。メソッドの中で、yield が使われてることで、メソッドにブロックが与えることを想定しているがわかる。
( cf. jutememo's gist: 1350439 — Gist )
each メソッドを利用してみる
以下の順に、実際に each メソッドを使ってみる。
- 「グループ」オブジェクトを生成し、
- 「人」オブジェクトを追加する。
- 「グループ」オブジェクトの each メソッドを呼出す。
aGroup = Group.new. add(Person.new("Tarou",21)). add(Person.new("Hanako",15)). add(Person.new("Jiro",15)) aGroup.each do |person| puts person.name end
Enumerable モジュールに定義されている collect , sort を使ってみる
Enumerable を Mixin すると、Enumerable モジュールで定義されている collect, sort などのメソッドを呼出すことができる。この2つのメソッドを試しに使ってみる。
p aGroup.collect{|person| person.age + 10} aGroup.sort{|a,b| a.age <=> b.age}.each do |person| print person.name, person.age, "\n" end
上記において、sort メソッドの呼び出しのとき、ソートの方法をブロックで渡している。
Enumerable - Rubyリファレンスマニュアル によると、
sort {|a, b| ... } 全ての要素を昇順にソートした配列を生成して返します。 ブロックなしのときは <=> メソッドを要素に対して呼び、その結 果をもとにソートします。
ここでは、Person クラスにおいて、<=> を定義してないので、上記のように、要素を比較する方法を、ブロックを渡す必要があった。
3. オブジェクトを比較するクラスに Comparable モジュールをインクルード
Enumerable の Mixin の方法がわかったところで、今度は上記の sort メソッドで利用された、「人」を比較するためのメソッドを「人」クラスに追加してみる。「人」オブジェクトを比較可能にするには、Comparable モジュールを Person クラスに Mixin する。
一例として、「人」を比較するためルールを、以下のように決める。
- 「はじめに `年齢' で比較し、
- もし同じ年齢だったら、`名前' のアルファベット順で比較する」
class Person include Comparable attr_reader :name , :age def initialize(name,age) @name = name @age = age end def <=>(other) cmp = @age <=> other.age if cmp != 0 return cmp else return @name <=> other.name end end end tarou = Person.new('Tarou',21) jirou = Person.new('Jirou',15) hanako = Person.new('Hanako',15) p tarou > jirou p tarou < jirou p tarou == jirou p jirou > hanako p jirou < hanako p jirou == hanako
Enumerable モジュールは、each メソッドを利用して、様々なメソッドが定義されている。Comparable モジュールでは、<=> を利用して、いくつかの操作が定義されている。
上記のように、Person に <=> を実装したので、Group に Mixin した Enumerable モジュールの sort メソッドを、ブロックなしで呼出せるようになった。
( cf. jutememo's gist: 1350439 — Gist )
4. モジュールは、Template Method パターンを使っている
このようなモジュールのインクルードの動作の仕組みは、「まつもと直伝 プログラミングのオキテ 第9回:ITpro」 に、次のように書かれている。
Rubyのクラス・ライブラリでTemplate Methodパターンを最も活用している部分は,EnumerableモジュールとComparableモジュールでしょう。...
Comparableモジュールは大小比較の基本となる「<=>」メソッドを用いて,各種比較演算を提供しています。「<=>」メソッドの仕様はレシーバと引数を比較して,レシーバの方が大きければ正の整数,等しければゼロ,小さければ負の整数を返すというものです。このメソッドを基礎にして, Comparableモジュールは「==」,「>」,「>=」,「<」,「<=」,「between?」の6つの比較演算を提供しています。
Ruby で書かれたデザインパターン (Template Method) の実装例を見たい場合は、以下を参照にすると良い。
前者は、Module を Mixin して、Template Method を実現しており、後者は、サブクラス化によって実現している。ただし、Mixin とは、
一般にTemplate Methodは継承とセットで語られますが,Enumerableのようにインクルードするだけで継承関係に関わりなく任意のクラスに機能を追加できるのは魅力的です。もっとも,Rubyのインクルードは一種の(制限された)多重継承なので,何の不思議もありませんが。
( cf. まつもと直伝 プログラミングのオキテ 第9回:ITpro )
5. Yield はどのように呼び出され、動作しているのか?
Enumerable モジュールは、C で実装されているらしい。
Nabble - [ruby-dev:32712] Re: Enumerable can't take multiple parameters によると、
たとえばEnumerable#collectをRubyで定義する時にどのように書くか、という問題です。現在はmodule Enumerable def collect result = [] self.each{|x| result.push(yield x)} result end endと等価になるように定義しています。
先ほど Group クラスで定義した each メソッドを参照しながら、 Enumerable の collect メソッドを見てみる。頭が混乱してくる。。 パタッ(o_ _)o~†
以下のように collect メソッドの呼出しをイメージしてみる。
うーん、ややこしい (@_@;) どこが一番想像しにくいのだろうか?順に動作を追って考えることにした。
- Group クラスに Mixin した collect メソッドを、ブロック付きで呼出す。
- Enumerable モジュールの collect メソッドの中で、 Group メソッドで定義した each メソッドを呼出す。
- Group クラスに定義した each メソッドでは、Person 要素を一つずつ取り出し、each メソッドに渡されたブロックの処理を進める。
例えば、先ほどの実行例で考えると、先頭の要素 `Tarou' が取り出されたとき、渡されたブロックの引数を展開すると、次のような値にが与えられているとイメージする。
{|tarou| result.push (yield tarou)}
ここでややこしいのは、yield が呼ばれていること(4)。
展開すると、こんな感じになるだろうか。
{|tarou| tarou.age + 10}
動作がわかりにくので、上記二つをまとめると、
{|tarou| result.push (tarou.age + 10)}
これを Person の要素分だけ繰り返して、 結果を 配列 result に詰め込んでいく。
Enumerable が関知していること
上記のように、個々の動作を追っていくと、何がなんだかわからなくなる。むしろ、Enumerable というモジュールの機能レベルで理解をしていく方がいいかもしれない。
Enumerable とは、 動詞 enumerate (列挙する、数え上げる) に由来する。「数え上げる」とは、対象がどのようなものであっても、複数の要素に対して行う行為を指す。
collect という操作であるなら、
「要素に対して、何らかの操作した結果の、集合を返す」
役割を持つと考える。要素を取り出す仕事は each が行う。
collect の中で呼出されている、each メソッドに渡されるブロック変数 x は、
「 x の中身が何なのか自分は関知しない」
ということを表わしている。その何だかよくわからない任意のものを、自分が呼出されたときに、与えられたブロックに渡し、「何らかの処理」を行わせた結果を受け取る。「何らかの処理」については、 Enumerable 自身は関知しない。ただ、「何らかの処理」の結果を受け取って、配列に詰め込むだけの役割を持つ。
6. 他の言語との比較
Haskell の map 関数と比較
Haskell にも、Ruby の Enumerable モジュールの collect に相当する map 関数がある。
map 関数の定義を見てみと、
ふつうのHaskellプログラミング (p76) によると、
map::(a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs
なんとシンプルな。。 (@_@;)
対象が何であれ、
「要素に対して、関数を適用する」
ということが素直に示されている。この定義を理解しておいてから、 Ruby の Enumerable モジュールの collect メソッドを見ると、何となく同じような形に見えてくる。yield x という文が、要素 x に対して関数を適用していることに等しい。
Haskell が再帰によって、要素に関数を適用するというところと、 Ruby が each によって、個々の要素を取り出し、順にブロックを適用するというところが違うけれど。
Java との比較
自分の場合、プログラミングは、 Java から入ったので、 Ruby のモジュールを見たときは違和感があった。Java の視点から見ると、 interface が外面だけでなく、中身も持っている感じ。Ruby はモジュールがあるから、Java よりも抽象的な表現による、コーディング促進されるのだろうか?
Java でオブジェクトを比較する方法を実装する場合、
- インターフェイス Comparable の compareTo を実装することによって、オブジェクト同士が比較可能にする。
- Arrays.sort, Collections.sort を利用してソートする。
- ソートするときに、Comparator を実装した匿名クラスのオブジェクトを渡せば、ソートの方法を変更することができる。これは、Ruby でブロックつきで呼出しているようなものと考えればよい。
ただし、クロージャと匿名インナークラスとの違いについては、MF Bliki: Closure に指摘されている。、
if you're a Java programmer you probably think "I could do that with an anonymous inner class", a C#er would consider a delegate.These mechanisms are similar to closures, but there are two telling differences.(...)
その二点とは、
closures can refer to variables visible at the time they were defined ...
Languages that support closures allow you to define them with very little syntax.
「Ruby のブロックと Proc」へつづく…