ラベル 等値 の投稿を表示しています。 すべての投稿を表示
ラベル 等値 の投稿を表示しています。 すべての投稿を表示

2008年3月30日日曜日

Ruby のイテレータ (2) – Enumerable と Comparable モジュール

Ruby のブロック付きメソッドとイテレータ - yield の様々な使い方」のつづき

1. 要素を保持する親クラスと、要素となる子クラス

080329-004次のような例を想定する。

  1. 「人」が「グループ」 に所属している。
  2. 「人」は `名前' と `年齢' を属性として持つ。

「グループ」クラスは親クラスで、「人」クラスは要素となる子クラス。

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 モジュールを「グループ」にインクルードする方法は、

  1. 「グループ」クラス内に、include Enumerable と記述。
  2. 「グループ」に、「人」の集合の要素を順番に取り出す 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 メソッドを使ってみる。

  1. 「グループ」オブジェクトを生成し、
  2. 「人」オブジェクトを追加する。
  3. 「グループ」オブジェクトの 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 する。

一例として、「人」を比較するためルールを、以下のように決める。

  1. 「はじめに `年齢' で比較し、
  2. もし同じ年齢だったら、`名前' のアルファベット順で比較する」
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 メソッドの呼出しをイメージしてみる。

080330-001

うーん、ややこしい (@_@;)  どこが一番想像しにくいのだろうか?順に動作を追って考えることにした。

  1. Group クラスに Mixin した collect メソッドを、ブロック付きで呼出す。
  2. Enumerable モジュールの collect メソッドの中で、 Group メソッドで定義した each メソッドを呼出す。
  3. 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 でオブジェクトを比較する方法を実装する場合、

  1. インターフェイス Comparable の compareTo を実装することによって、オブジェクト同士が比較可能にする。
  2. Arrays.sort, Collections.sort を利用してソートする。
  3. ソートするときに、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」へつづく…

 

連想事項