メソッドが見つからないならRubyに作ってもらえばいいよ! - If method_missing, define_method by Ruby -
メソッド定義
Rubyのオブジェクトはメッセージに反応する
つまりオブジェクトがメッセージを受けると
オブジェクトは対応するメソッドを見つけてその結果を返す
Rubyではオブジェクト自身はメソッドを持っていない
だからオブジェクトは自身が属するクラスにアクセスして
対応するメソッドを得てその結果を返す
つまりRubyのメソッドはクラスに定義される
メソッド定義はdef文で行う
class Person def name(arg) "My name is #{arg}" end end my = Person.new my.name "Charlie" # => "My name is Charlie"
特定のクラスで定義されたメソッドは
そのクラスから生成されるオブジェクトで使えるようになる
Rubyではすべてのクラスは
Classクラスから生成されたオブジェクトである
だからClassクラスで定義されたメソッドは
すべてのクラスで使えるようになる
class Class def mother "My mother is #{self.class}" end end class Person p mother end # >> "My mother is Class"
もちろんClassクラスでも!
class Class p mother end # >> "My mother is Class"
singletonクラスでのメソッド定義
通常一つのクラスからは複数のオブジェクトが生成されるので
それらのオブジェクトは同じメソッドを共有することになる
だけど
特定のオブジェクトだけが使えるメソッドを定義したい場合もある
そのためにRubyではsingletonクラス(特異クラス)が用意されている
my = Person.new his = Person.new class << my def secret "My account number is #100789" end end my.secret # => "My account number is #100789" his.secret # => 15: undefined method `secret' for #<Person:0x23168> (NoMethodError)
略記法もある*1
my = Person.new def my.secret #myのselfメソッドにする "My account number is #100789" end my.secret # => "My account number is #100789"
前にも書いたようにRubyではクラスもオブジェクトだから
クラスに対してもsingletonクラスを用意して
そこでメソッドを定義できる
このsingletonクラスで定義されるメソッドは
そのクラスだけが使えるメソッドになる
class Person end class << Person def secret "My secret code is #{self.to_s.reverse}." end end Person.secret # => "My secret code is nosreP."
略記法のほうが広く使われている
class Person def self.secret "My secret code is #{self.to_s.reverse}." end end Person.secret # => "My secret code is nosreP."
このようなクラスが使うメソッドは
一般にクラスメソッドと呼ばれるけれど
機能上普通のオブジェクトが使うメソッドと変わりはない
もちろんClassクラスもオブジェクトだから
singletonクラスを定義できる
class Class def self.secret "My secret code is #{self.to_s.reverse}." end end Class.secret # => "My secret code is ssalC."
モジュールにおけるメソッド定義
Rubyにはモジュールというクラスに似たオブジェクトがある
モジュールはクラスと違ってオブジェクトを生成できないけれども
そこにメソッドを定義することはできる
特定のクラスに特定のモジュールをincludeすることによって
そのクラスから生成されるオブジェクトにおいて
モジュールに定義されたメソッドが使えるようになる
module Secret def secret "My secret code is #{self.to_s.reverse}." end end class Person include Secret end my = Person.new my.secret # => "My secret code is >46a32x0:nosreP<#."
モジュールもクラス同様オブジェクトだから
モジュールに対してもsingletonクラスを定義できる
module Secret end class << Secret def secret "My secret code is #{self.to_s.reverse}." end end Secret.secret # => "My secret code is terceS."
クラス同様略記法の方が一般的だ
module Secret def self.secret "My secret code is #{self.to_s.reverse}." end end Secret.secret # => "My secret code is terceS."
モジュールはModuleクラスから生成されるので
Moduleクラスに定義されたメソッドは
すべてのモジュールで使えるようになる
class Module def secret "My secret code is #{self.to_s.reverse}." end end module Secret end Secret.secret # => "My secret code is terceS."
なおClassクラスはModuleクラスのサブクラスだから
Moduleクラスに定義されたメソッドは
Classクラスに定義されたメソッドとなり
従ってすべてのクラスでも使えるようになる
class Module def secret "My secret code is #{self.to_s.reverse}." end end class Person end Person.secret # => "My secret code is nosreP."
動的メソッド定義 -define_method
メソッドはクラスにdef文で定義すると書いた
つまり通常メソッドはクラスの設計時に定義される
だけどメソッドを必要に応じて後から定義したい場合もある
そのような場合メソッド定義をネストすればいい
class Person def method_maker def name(word) "My name is #{word}" end end end my = Person.new my.method_maker my.name "Charlie" # => "My name is Charlie"
こうすれば外側のメソッドが呼ばれたとき
初めて内側のメソッドが定義されるようになる
でもせっかく後から定義するんだから
もっと柔軟に定義できたらうれしい
例えばメソッド名をユーザの入力で決めるとか
それに答えるのがModuleクラスのdefine_methodだ
class Person def method_maker(meth_name) define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end end my = Person.new my.method_maker(:name) my.name "Charlie"
これでmethod_makerによってnameメソッドが作られて
myオブジェクトから呼べるようになる
# ~> -:5:in `method_maker': undefined method `define_method' for #<Person:0x23f14> (NoMethodError) # ~> from -:12
残念ながらこれはうまくいかないようだ
エラーメッセージをよく見ると
Personクラスのオブジェクトに対して
define_methodを呼んでるようだ
つまりdef文の中のコンテキストは
Personクラスのオブジェクトということらしい
確かめてみよう
class Person def method_maker(meth_name) p self end end my = Person.new my.method_maker(:name) # >> #<Person:0x23c08>
やっぱりそうだ
def文の中ではselfはPersonクラスのオブジェクトになってる
Personクラスにメソッドを定義したいんだからそれじゃだめだ
Personクラスのコンテキストでdefine_methodを呼びたいんだ
こんなときはeval系メソッドが使える
class_evalはクラスのコンテキストで渡されたブロックを評価する
だからPersonクラスに対してclass_evalを呼び出して
そのブロックの中でdefine_methodを定義すればいい
class Person def method_maker(meth_name) Person.class_eval do define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end end end my = Person.new my.method_maker(:name) my.name "Charlie" # => "My name is Charlie." my.method_maker(:old_name) my.old_name "Henry" # => "My old_name is Henry." my.method_maker(:size) my.size "XXL" # => "My size is XXL."
クラスのユーザが後からメソッドを追加できるなんて素敵だ
method_makerに可変長引数を渡せるようにすれば
もう少しよくなる
class Person def method_maker(*meth_names) meth_names.each do |meth_name| Person.class_eval do define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end end end end my = Person.new my.method_maker :name, :old_name, :size my.name "Charlie" # => "My name is Charlie." my.old_name "Henry" # => "My old_name is Henry." my.size "XXL" # => "My size is XXL."
ここでdefine_methodは
Personクラスのコンテキストで評価されているので
Personクラスの特定のオブジェクト(my)で
method_makerを呼ぶと
Personクラスの他のオブジェクトでは
既にname, old_name, sizeの各メソッドが使える
his = Person.new his.name "Peter" # => "My name is Peter." his.old_name "arthur" # => "My old_name is arthur." his.size "M" # => "My size is M."
でも各オブジェクト毎に使えるメソッドを変えたい場合もある
そんなときはオブジェクトのsingletonクラスのコンテキストで
define_methodが評価されるようにすればいい
class Person def method_maker(*meth_names) obj_singleton = class << self; self end meth_names.each do |meth_name| obj_singleton.class_eval do define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end end end end my = Person.new my.method_maker :name, :old_name, :size my.name "Charlie" # => "My name is Charlie." my.old_name "Henry" # => "My old_name is Henry." my.size "XXL" # => "My size is XXL." his = Person.new his.name "Peter" # ~> -:25: undefined method `name' for #<Person:0x1f824> (NoMethodError) his.method_maker :name, :age, :address his.name "Peter" # => "My name is Peter." his.age 21 # => "My age is 21." his.address "New York" # => "My address is New York."
動的メソッド定義 -method_missing
ここまで来ると欲が出てくる
メソッドを追加するのにいちいちmethod_makerメソッドを
呼ばなければならないのはスマートじゃない
そこでmethod_missingメソッドの出番だ
method_missingは呼び出されたメソッドが未定義のときに
Rubyが自動で起動するフックメソッドだ
class Person < BasicObject def initialize(*meths) meths.each { |meth| __send__(meth) } end def method_missing(meth_name) obj_singleton = class << self; self end obj_singleton.class_eval do define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end end end my = Person.new :name, :old_name, :size my.name "Charlie" # => "My name is Charlie." my.old_name "Henry" # => "My old_name is Henry." my.size "XXL" # => "My size is XXL." his = Person.new :name, :age, :address, :class his.name "Peter" # => "My name is Peter." his.age 21 # => "My age is 21." his.address "New York" # => "My address is New York." his.class "Premier" # => "My class is Premier."
上のコードではinitializeメソッドにおいて
引き渡されたメソッド名を呼び出すようにしている
こうすればオブジェクトの生成時にmethod_missingが呼ばれて
対応するメソッドが定義されるようになる
でもユーザがクラス階層にある
既存のメソッドを引き渡した場合
Rubyはそのクラス階層をすべて探索して
そのメソッドを見つけるからmethod_missingは当然呼ばれない
例のようにPersonクラスをBasicObjectのサブクラスにすれば
そのリスクは最小になる(例ではObject#class)
さらにmethod_missingが
定義したメソッドの結果を返すようにすれば
オブジェクト生成時にメソッド名を渡す必要もなくなる
class Person < BasicObject def method_missing(meth_name, word) obj_singleton = class << self; self end obj_singleton.class_eval do define_method(meth_name) do |word| "My #{meth_name} is #{word}." end end __send__("#{meth_name}", word) #定義したメソッドを返す end end my = Person.new my.name "Charlie" # => "My name is Charlie." my.old_name "Henry" # => "My old_name is Henry." my.size "XXL" # => "My size is XXL." his = Person.new his.name "Peter" # => "My name is Peter." his.age 21 # => "My age is 21." his.address "New York" # => "My address is New York." his.class "Premier" # => "My class is Premier."
上の例では未定義のinstanceメソッドの呼び出しに対して
method_missingが起動して
対応するinstanceメソッドが定義されるようにした
こうなると未定義のクラスメソッドの呼び出しに対しても
method_missingを起動させて
対応するクラスメソッドを定義できるようにもしたくなる
その場合method_missingおよびdefine_methodを
クラスのsingletonクラスのコンテキスト
つまりクラスメソッドで定義すればいい
class Country < BasicObject def self.method_missing(meth_name) cls_singleton = class << self; self end cls_singleton.class_eval do define_method(meth_name) do |word| instance_variable_set("@#{meth_name}", word) end end end capital; language; population end class Japan < Country capital "Tokyo" language "Japanese" population 127433494 def self.to_s "Japan\n Capital: #{@capital}\n Language: #{@language}\n Population: #{@population}" end end class Denmark < Country capital "Copenhagen" language "Danish" population 5475791 def self.to_s "Denmark\n Capital: #{@capital}\n Language: #{@language}\n Population: #{@population}" end end puts Japan puts Denmark # => Japan Capital: Tokyo Language: Japanese Population: 127433494 Denmark Capital: Copenhagen Language: Danish Population: 5475791
この例ではCountryクラスの最後で
クラスメソッドを呼ぶことでそれらを定義し
CountryクラスのサブクラスであるJapanとDenmarkで
それらのメソッドを使っている
定義したメソッドはメソッド名のインスタンス変数に引数をセットする
なおBasicObjectは他のクラス同様
Classクラスのオブジェクトなので
instanceメソッドの場合と異なって
そこから継承された多数のクラスメソッドがある
だからそれらとの衝突が起きないよう注意が必要だ
まとめ
- メソッドはクラスに定義される
- クラスに定義されるメソッドはそのクラスから生成される各オブジェクトで使える
- クラスはClassクラスのオブジェクト(インスタンス)だからClassクラスに定義されたメソッドは各クラスで使える
- singletonクラスに定義されるメソッドはそのオブジェクト専用になる
- クラスメソッドとはクラスのsingletonクラスに定義されたメソッドである
- モジュールに定義されるメソッドはそれがincludeされたクラスのオブジェクト(インスタンス)で使える
- メソッド定義をネストすれば外側のメソッドが呼ばれたときに内側のメソッドが定義されるようになる
- class_evalとdefine_methodを使って動的にメソッドを定義できる
- method_missingを使って自動的にメソッドが生成されるようにできる
追記2008-10-26:Countryクラスでclass_variable_setでなくinstance_variable_setを使うよう修正。
*1:適切じゃないけど理解を容易にするために