おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(15)モジュール

モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。

モジュールの定義

モジュールの定義とクラスの定義は似ています。 クラスの定義にはclassキーワードを使いますが、モジュール定義ではその代わりにmoduleキーワードを使います。

module Abc
  def abc
    print "abcdefg\n"
  end
end

モジュール名はAbcです。 モジュール名は定数で、最初の文字は大文字でなければいけません。 モジュールはクラスと違い、インスタンスを作ることはできません。 もし、Abc.newとしてインスタンスを作ろうとするとエラーになります。

インクルード

先程の例ではインスタンスメソッドabcをモジュールAbcの中で定義しました。 しかし、モジュールではインスタンスを作れないのですから、このメソッドは使いようがないように見えます。 メソッドを使うには、includeメソッドでモジュールを取り込む必要があります。

include Abc
abc

実行すると

abcdefg

と画面に現れます。 つまり、モジュールのメソッドabcを実行できたのです。

モジュールをインクルードすることにより、モジュールのメソッドを引き継ぐことができます。 通常はクラス定義の中でモジュールをインクルードしてメソッドを引き継ぎます。

class Xyz
  include Abc
  def xyz
    abc
  end
end

x = Xyz.new
x.abc
x.xyz

これを実行すると、クラスXyzでモジュールAbcがインクルードされることにより、モジュールのメソッドabcがクラスXyzのインスタンスをレシーバとして呼ぶことができるようになります。 プログラムを実行するとabcdefが2つ表示されます。

abcdef
abcdef
  • Xyzクラスのインスタンスxを生成
  • abcXyzインスタンスメソッドになったから、x.abcによってabcdefが表示される
  • インスタンスメソッドxyzの中でインスタンスメソッドabcを呼び出せる。 def xyz〜endでは、selfが指すのはxyzが呼び出されたときのレシーバ(インスタンス)である。 そして、メソッド定義の中でメソッド呼び出し(abc)があり、そのレシーバが省略されたときは、selfをレシーバとする。 プログラムリストの最後の行で、x.xyzが呼び出されたとき、その中で呼び出されたabcのレシーバはxになる(selfはxになっている)

なお、includeはメソッド定義の中では使えません。 クラス定義の中で、メソッド定義の外でインクルードしてください。

ミックスイン

これまでのところをまとめておきましょう。

このことから、モジュールの目的(のひとつミックスイン)はクラスのインスタンスメソッドを提供することです。 しかしここで次のような疑問が浮かびます。 最初からクラス内でメソッドを定義すればインクルードなど必要ないのに、なぜわざわざモジュールのメソッドを取り込むのでしょうか?

モジュールは特定のクラスのためにメソッドを定義しているのではなく、複数のクラスに提供するためにメソッドを定義しているのです。 例えば、Comparableという組み込みのモジュールがあります。

  • Comparableは<=>メソッドをもとに、==>>=<<=between?clampというメソッドを定義している
  • Comparableをインクルードするクラスには<=>メソッドが定義されていなければならない

Comparableは比較可能なオブジェクトである整数、実数、文字列などに適用されています。 また、ユーザが比較可能なクラスを作るとき、<=>メソッドを定義し、Comparableをインクルードするだけで、==などの7つのメソッドが使えるようになります。

例として、江ノ電の駅のオブジェクトを作り、Comparableをインクルードしてみましょう。 駅には駅番号があり、藤沢のEN01から鎌倉のEN15までとなっています。 この駅番号の大小で駅の大小を決めることにします。

class Enoden
  include Comparable

  attr_reader :station_number, :name
  def initialize station_number, name
    @station_number = station_number
    @name = name
  end
  def <=>(other)
    self.station_number.slice(2..3).to_i <=> other.station_number.slice(2..3).to_i
  end
  def to_s
    "#{@station_number}: #{@name}"
  end
end

station_number = (1..15).map{|i| sprintf("EN%02d", i)}
name = "藤沢 石上 柳小路 鵠沼 湘南海岸公園 江ノ島 腰越 鎌倉高校前 七里ヶ浜 稲村ヶ崎 極楽寺 長谷 由比ヶ浜 和田塚 鎌倉".split(/ /)

enoshima = Enoden.new(station_number[5], name[5])
inamuragasaki = Enoden.new(station_number[9], name[9])
kamakura = Enoden.new(station_number[14], name[14])

print enoshima, "\n"
print inamuragasaki, "\n"
print enoshima < inamuragasaki, "\n"
print [kamakura, enoshima, inamuragasaki].sort.map{|a| a.to_s}, "\n"

これを実行すると次のようになります。

EN06: 江ノ島
EN10: 稲村ヶ崎
true
["EN06: 江ノ島", "EN10: 稲村ヶ崎", "EN15: 鎌倉"]

プログラム中のattr_readerメソッドは読み出しのみサポートするインスタンス変数を定義します。 次のプログラムと同等です。

def station_number
  @station_number
end
def name
  @name
end

プログラムを説明します。

  • クラスEnodenはモジュールComparableをインクルードする
  • attr_readerメソッドによって、読み出しのみ可のインスタンス変数@station_number@nameを定義
  • オブジェクトを生成する時に、引数に駅番号、駅名を与える
  • 大小比較<=>は駅番号文字列の3〜4文字目を(sliceは0から数えるので2..3となっている)整数に直して比較
  • to_sメソッドは駅番号と駅名を文字列として返す
  • 駅番号の文字列の配列を作成
  • 駅名の文字列の配列を作成
  • 江ノ島稲村ヶ崎、鎌倉のEnodenオブジェクトを作成
  • enoshimaとinamuragasakiを(to_sを使って)プリント
  • enoshimaとinamuragasakiの大小比較。稲村ヶ崎の駅番号の方が大きいのでtrueが返る
  • kamakura, enoshima, inamuragasakiの配列を作り、ソート。 <=>が定義してあるので、ソートが可能。 to_sで駅番号、駅名の文字列に直して表示

今回の例では江ノ電だけを対象にしましたが、一般に日本の駅には駅番号(駅ナンバリング)が用いられるようになってきました。 駅ナンバリングは3字以内の英字(路線)と番号でできています。 これから、Enodenクラスをより一般的な駅を表すStationクラスに格上げすることも可能だと思います。 駅の比較は(1)英字をアルファベットで大小をつけ(2)番号で大小をつけるという2段階で実現できます

さて、具体的にComparableモジュールがクラスにインクルードされ、その実態(オブジェクト)を手に入れるのを見てきました。 比較可能なクラスは様々あり、それらすべてにComparableモジュールが適用できます。 このように、クラス共通のメソッドを定義したモジュールを、各クラスに適用することをミックスインといいます。

Comparableは既存のモジュールですが、ユーザが独自にモジュールを作り、複数のクラスに適用することもできます。 また、Comparable以外にもEnumerableという非常に有用なモジュールがあります。 このモジュールはeachメソッドのあるクラスに適用できます。 eachメソッドを作るにはブロック付きメソッドの定義を知らなければならないので、今回は解説しませんでしたが、別の記事で取り上げられればと思っています。