おもこん

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

徒然Ruby(21)特異メソッド、名前空間、モジュール関数

今回は特異メソッド、特異クラス定義、名前空間、モジュール関数について説明します。

特異メソッド

特異メソッドは英語ではsingleton method(一人っ子のメソッド)といいます。 英語の方が意味をとりやすいと思います。 特異メソッドは個々のオブジェクト(インスタンス)だけがレシーバになるメソッドのことを言います。

次の例はクラスAのインスタンスを2つ作り、そのうちの一方に特異メソッドを作ったときの実行結果を示すものです。 オブジェクトaに特異メソッドを定義するにはdef a.introのように「オブジェクト、ドット、メソッド名」のようにします。

class A
end

a = A.new
b = A.new

def a.intro
  print "私はaです\n"
end

a.intro #=> 私はaです
b.intro #=> エラー (NoMethodError)

実行します。

私はaです
example21.rb:12:in `<main>': undefined method `intro' for #<A:0x00007f6f3d737bd0> (NoMethodError)

b.intro
 ^^^^^^

aにはintroメソッドを定義したので「a.intro」の結果「私はaです」が出力されましたが、bには定義されていないので、「b.intro」はエラーになりました。

通常のメソッドは同じクラスから生成したインスタンスすべてで可能ですが、特異メソッドはそれが定義された個別のインスタンスでのみ可能です。 特異メソッドは様々なケースで使えると思いますが、例えば次のようなケースが考えられます。

  • クラスUserはあるシステムを使用するユーザを定義する
  • 個々のユーザはUserクラスのインスタンスとして生成する
  • ユーザの中には一人だけ管理者ユーザがいる。つまりUserクラスのインスタンスの中にひとつだけ管理者ユーザ・インスタンスがある
  • 管理者ユーザ・インスタンスには特別のメソッドを付与する(特異メソッドになる)

クラスメソッド

Rubyでは個々のクラス(例えばIntegerやString)はClassクラスのインスタンスです。 話がややこしくなりますが、クラスはクラスであると同時にインスタンスでもあるわけです。

クラス名は大文字で始まりますが、この文字は定数を表しています。 クラスを定義すると、そのクラスオブジェクト(Classクラスのインスタンス)が生成され、定数に代入されるのです。 つまり大文字で始まる名前は、クラスを表す名前であると同時に定数の名前でもあります。

クラスはインスタンスですから、変数に代入することもできます。

class A
end

c = A
p c  #=> A
p c.class #=> Class

変数cにはクラスA(のオブジェクト)が代入され、「p c」でクラス名Aが、「p c.class」でクラスAのクラスであるClassが表示されます。

このようにクラスを変数に代入することはできますが、通常はそういうことはしません。 それはプログラムを分かりにくくなることに繋がります。 定数を参照すれば、それがクラスだと直感的に分かります。 定数だけをクラスに用いるのが良い習慣です。

さて、クラスはオブジェクトですから、特異メソッドを定義することができます。 クラスの特異メソッドをクラスメソッドといいます。

class A
end

def A.intro
  print "私はクラスAです\n"
end

A.intro #=> 私はクラスAです
selfを使った定義

クラスメソッドの定義はクラスの内側でもできます。

class A
  def A.intro
    print "私はクラスAです\n"
  end
end

A.intro #=> 私はクラスAです

この方法では疑似変数selfを使うことができます。 「class A〜end」では、selfは「ClassクラスのインスタンスであるクラスA」を指します。 したがって、この区間では定数Aとselfは同じオブジェクトを指します。 ですので、

class A
  def self.intro
    print "私はクラスAです\n"
  end
end

A.intro #=> 私はクラスAです

のように、selfを使ってクラスメソッドを定義することができるわけです。

クラスメソッドの継承

クラスメソッドはそのサブクラスにも継承されます。 上記のプログラムに続き、以下を追加するとBにもAのクラスメソッドintroが継承されます。

class B < A
end
B.intro #=>私はクラスAです
モジュールの特異メソッド

特異メソッドは個々のオブジェクトにメソッドを定義するものでした。 クラスメソッドはクラス(クラスはオブジェクトでもある)の特異メソッドです。 ではモジュールについてはどうでしょうか? モジュールはクラス同様にオブジェクトなのでしょうか? 答えはイエスです。 モジュールはModuleクラスのインスタンスです。 したがって、モジュールの特異メソッドを作ることができます。

module C
end

def C.intro
  print "私はモジュールCです\n"
end

C.intro #=> 私はモジュールCです

モジュール定義の中ではselfを使った特異メソッド定義が可能です(クラスの場合と同じ)。

モジュールの場合はクラスと違い、サブモジュールを作ることができませんから、特異メソッドの継承はありません。 また、モジュールをincludeしても特異メソッドの継承はできません。 インクルードは通常のメソッドと定数を引き継ぐだけです。

名前空間

トップレベルに同じ名前のメソッドを2つ作ることはできません。 それをすると、2番めの定義が1番めの定義の「再定義」になり、以後2番めの定義だけが有効になります。 しかし、異なる動作をする2つのメソッドに同じ名前をつけたいときもあります。 例えば、あるプログラムはMarkdownファイルをHTMLにもPDFにも変換できるとします。 「変換する」メソッドは、convertという名前が良いでしょう。 HTMLに変換するのもconvert、PDFに変換するのもconvertにしたいのです。

このとき、convertにプリフィックスをつけて区別することが可能です。

def html_convert
... ... ...
end
def pdf_convert
... ... ...
end

このようなプリフィックス名により、区別することを「名前空間」を使うといいます。 HTMLに関するメソッドにはすべてhtml_をつけ、PDFに関するメソッドについてはpdf_をつけるわけです。

同様のことはmoduleの特異メソッドでも実現できます。

module HTML
end
module PDF
end

def HTML.convert
... ... ...
end
def PDF.convert
... ... ...
end

このようにモジュールの特異メソッドは名前空間を付与することと同一の効果があります。 同じことをクラスでもできますが、クラスには継承があるため、サブクラスを作ると別の名前空間で同じ内容のものが作られてしまいます。 したがって、名前空間としてはモジュールの利用が適しています。

しかし、クラスの特異メソッドも名前空間に似た効果があります。 例えば、IOクラスにはreadという特異メソッドがあります。

s = IO.read(ファイル名)

このメソッドはファイルを読み込み、その文字列を変数sに代入します。

単にreadという名前のメソッドでは名前の衝突が不安になりますが、IO.がついているので、これはIOクラスの特異メソッドだと分かります。 これは名前空間に似た効果です。 また、IOのサブクラスにFileがあります。 特異メソッドはサブクラスに引き継がれるので

s = File.read(ファイル名)

でも同じreadメソッドが呼ばれます。 これはファイルクラスの特異メソッドと見えますが、実はIOの特異メソッドの継承です。

特異メソッドは一般のオブジェクト(通常のクラスから生成したインスタンス)に定義するよりも、モジュールやクラスに定義する場合が多いように思います。 実際Rubyのドキュメントを見ると、クラスメソッドは数多く定義されています。 最もよく使うのはインスタンスを生成するnewメソッドでしょう。 これはほとんどのクラスの祖先であるObjectクラスの特異メソッドです。 各クラスはそれを受け継いでいるので、newが使えるのですね。

特異クラス定義

特異クラス定義とは、次のようにclass <<(式)〜endという構文のことをいいます。 ここで定義されるメソッドは(式)の指すオブジェクトの特異メソッドになります。

class A
end
a = A.new
b = A.new
class << a
  def intro
    print "私はaです\n"
  end
end
a.intro #=> 私はaです
b.intro #=> エラー(NoMethodError)

classに<< aをつけると、aの指すオブジェクトについて定義をすることになります。 そこで定義されるメソッドはすべてaの指すオブジェクトの特異メソッドです。 この記法の良いところは「多くの特異メソッドを少ないタイプ量で記述できる」ことです。

また、selfをこの記法に使い、クラスメソッドを定義することができます。

class A
  class << self
    def intro
      print "私はクラスAです\n"
    end
  end
end
A.intro #=> 私はクラスAです

これにより、class A〜endの中で、普通のメソッドもクラスメソッドも定義することができ、非常に効率的になります。 この記法が最も良いかもしれません。 モジュールにはこのタイプの記法がないので、個別に作ることになります。 ですが、次に述べるモジュール関数を作る方法を適用すると簡単に特異メソッドと通常のメソッドの2つができます。

モジュール関数

ビルトイン・オブジェクトの中にMathモジュールがあります。 数学関数を提供するモジュールです。 例えば正弦関数はMath.sin(角度)として呼ぶことができます。 また、モジュールをインクルードしておけば、sin(角度)とモジュール名を省略することができます。

p Math.sin(Math::PI/3) #=> 0.8660254037844386
include Math
p sin(PI/3) #=> 0.8660254037844386
  • Math::PIはモジュールMathの中で定義されている定数。 定数の場合はドットではなく::を使ってモジュール下の定数であることを示す。 クラス定義の中で定義された定数も「クラス名::定数」となる。 これはMathモジュール外部からの参照で用いられる
  • includeによって、モジュール内の定数やメソッドが取り込まれる。 PIにMath::を付けなくて良い。 また、sinメソッドはMathモジュールの通常のメソッドである

ここで注意が必要なのは、Math.sinとsinメソッドの違いです

  • Math.sin=>Mathモジュールの特異メソッド
  • sin=>Mathモジュールの通常のメソッドがincludeで取り込まれたもの

この2つがセットになっているので、インクルードを使う/使わないの選択が可能になります。 これは便利なことなので、モジュールの提供するメソッドに名前空間をつける目的であれば、両方のメソッドの定義をするべきです。 (Math.がつかないsinはインクルードが必要なので、その時点で名前の衝突を避けることができる)。 このようなメソッドを「モジュール関数」といいます。

以前紹介したモジュールのミックス・インは通常のメソッドを複数のクラスに提供する仕組みでした。 ミックス・インのメソッドは、特異メソッドを含まないので、モジュール関数ではありません。

モジュール関数は通常のメソッドと特異メソッドの2つを定義しなければならないのですが、それは非効率です。 それを解決するメソッドがmodule_functionメソッドです。

  • 通常のメソッドを定義する
  • module_function メソッド名によって、そのメソッドがモジュール関数になる。 メソッド名はシンボルで表す
module X
  def intro
    print "私はモジュールXのモジュール関数です\n"
  end
  module_function :intro
end
X.intro #=> 私はモジュールXのモジュール関数です
include X
intro #=> 私はモジュールXのモジュール関数です

有用な関数をプログラム全体で使えるように提供したいときには、このようなモジュール関数として提供するのが良い方法です。 特にライブラリの作成においては、モジュール関数にするのがベストな選択です。 (ライブラリでなければ、トップレベルのメソッドでも十分です)。

定数の名前空間

前項で述べたように、モジュール内で定義された定数をモジュール外から参照するには「モジュール名::定数名」としなければなりませんでした。 これは「モジュールで、定数にも名前空間をつけることができる」ということを示しています。

定数は変数と同様にオブジェクトを指しますが、変数と違い再代入ができません。 その定数が生きている間はずっと同じオブジェクトを指します。 ユーザが定義する定数は数字や文字列(特にフリーズした文字列)が多いと思いますが、その他にクラス名やモジュール名も定数です。 このことから「クラスやモジュールにも名前空間を適用することができる」ということができます。

module ABC
  class A
    def intro
      print "モジュールABCのAです\n"
    end
  end
end
module EFG
  class A
    def intro
      print "モジュールEFGのAです\n"
    end
  end
end

a = ABC::A.new
b = EFG::A.new
a.intro #=> モジュールABCのAです
b.intro #=> モジュールEFGのAです

プログラムが大きくなり、クラス名の名前の衝突が心配であれば(特にライブラリでは)モジュールを使った名前空間の提供が解決手段となるかもしれません。