タイトル
メタプログラミングRuby
著者
Paolo Perrotta (著), 角征典 (翻訳)
出版社
アスキー・メディアワークス
Amazonで購入する

Ruby が人気のあるプログラミング言語である理由のひとつに、強力なメタプログラミングがあると思います。Ruby の本当の力を知るのは、このメタプログラミングを理解したときだとも思います。

本書は、Ruby の内部動作をきちんと説明した上で、メタプログラミングの説明をしてくれます。とても丁寧な説明で、少しでもプログラミングの知識があれば理解できる文章になっています。

プログラミング能力を高めるひとつの方法に、良いソースコードを読むことというのがあります。Ruby で書かれたフレームワークやライブラリのソースコードを読むと、なんでこんな動作になるんだろう?という疑問や、どうしてこんな風に書けるのだろう?という疑問が湧いてきます。

本書を読むと、そういった疑問の一つ一つが本当に紐を解くように理解できるようになります。Ruby の基礎を勉強した初級者が中級者になろうと思ったときに、ぜひ読むべき本だと思います。

本書のおぼえがきです。

本書のまとめ

クラスはオブジェクト

オブジェクトにクラスがあるように、クラスにもクラスがあります。クラスのクラスは Class クラスです。

インスタンス変数はインスタンス、メソッドはクラスの持ち物

インスタンス変数はインスタンスごとにもっていますが、メソッドはインスタンスのクラスに定義されます。

class キーワードはクラスを定義するのではなくクラスをオープンする

class キーワードはクラスをオープンします。既存のクラスに対して使えば再オープンできて、メソッドの追加やモンキーパッチができるし、新規のクラスに対して使えば、そのクラスを定義することになります。

カレントオブジェクトとカレントクラス

Ruby のプログラムは常にカレントオブジェクト self とカレントクラスを持っています。

self が何を指すかに注意する

Ruby はクラスもオブジェクトなので、class キーワードでクラスをオープンすると self はクラスを指し示します。

メソッドを定義するとカレントクラスのインスタンスメソッドになる

def で定義されたすべてのメソッドはカレントクラスのインスタンスメソッドになります。

また、メソッドはインスタンスメソッドになるので、メソッドの中では self はメソッドを呼び出したインスタンス自身を指すことになります。

クラスのインスタンスの特異メソッドとクラスメソッドは同じ仕組み

クラスのインスタンスに特異メソッドを定義するには、def obj.my_method のようにします。

クラスもオブジェクトなので、同じように特異メソッドを定義できます。def MyClass.my_method というのは MyClass で指し示しているオブジェクト(つまりクラス)に特異メソッドを定義していることになります。

class キーワードでクラスをオープンすると、クラス定義のスコープでは、self はカレントクラスを指すので、

class MyClass
  def self.my_method
  end
end

def MyClass.my_method
end

は同じことをしていることになります。

特異メソッドは特異クラスに定義される

メソッドを定義するとクラスのインスタンスメソッドになります。特異メソッドも例外ではなく、クラスのインスタンスメソッドとして定義されます。

では、特異メソッドはどのクラスのインスタンスメソッドになるかというと、特異メソッドは特異クラスという特別なクラスのインスタンスメソッドとして定義されます。

クラスの特異クラスにメソッドを定義することは、クラスメソッドを定義すること

クラスメソッドはクラスのインスタンスに特異メソッドを定義するのと同じ方法で定義されることは、上にもまとめました。

つまり、クラスメソッドはクラスの特異クラスにメソッドを定義することと同じなので、特異クラスにインスタンスメソッドを定義することはクラスメソッドを定義することになります。

class MyClass
  class << self
    def hello
    end
  end
end

で、MyClass.hello が定義できます。

特異クラスのスコープに入るには class << obj 構文を使う

特異クラスのスコープに入るには class << obj 構文をつかいます。特異クラスはインスタンスをひとつしかもてません。そのため、シングルトンとも呼ばれます。

private メソッドのルール

private メソッドは明示的なレシーバをつけて呼び出すことができないというものです。

Ruby のルールでは他のオブジェクトのメソッドを呼び出すにはレシーバを明示しなければいけないので、private メソッドは他のオブジェクトからは呼び出せません。

オープンクラス

Ruby は、既存のクラスを再オープンしていつでもクラスを修正することができる、オープンクラスという技術があります。このオープンクラスを使って、既存のクラスのバグを修正したり、既存のクラスにメソッドを追加したりすることをモンキーパッチと呼びます。

オープンクラスの使い方

Ruby の class キーワードは、クラス宣言というよりもスコープ演算子のようなものである。もちろん、存在しないクラスは作成する。しかしそれは、副作用と言ってもいいかもしれない。class の主な仕事は、あなたをクラスのコンテキストにつれていくことである。そのコンテキストであなたがメソッドを定義する。

クラス定義の中身 - 本書P.41

Ruby でクラスの再オープンをするには、次のように通常のクラス宣言のように書きます。

class String
  def hoge
    # 追加したいメソッドの中身
  end
  
  def replace   # <-- すでに String クラスに定義されている
    # モンキーパッチしたい中身
  end
end

1行目で String クラスを再オープンして、 2行目で hoge メソッドを String クラスに追加しています。また、6行目で既存のメソッド replace を再オープンしてモンキーパッチしています。

インスタンス変数、メソッド、クラス、定数、モジュール

インスタンス変数

インスタンス変数とは、クラスのインスタンスがもつ変数のことです。オブジェクトに対して、instance_variables() を呼び出すことで、そのオブジェクトが持つインスタンス変数の一覧を取得できます。インスタンス変数はインスタンスに所属します。

メソッド

メソッドには、インスタンスメソッドとクラスメソッドがあります。Ruby ではメソッドはクラスに所属します。そのため、クラスには Object.instance_methodsObject.methods という2つのメソッドが用意されています。

String.instance_methods == "hoge".methods  # true

メソッドはクラスに所属するのでインスタンス間で共有されますが、インスタンス変数はインスタンスごとに割り当てられるため値がごっちゃになることはありません。

クラス

クラスはオブジェクトである。だから、オブジェクトに当てはまるものはクラスにも当てはまる。オブジェクトと同じように、クラスにもクラスがある。クラスは Class クラスのインスタンスなのだ。

"hello".class    # String
String.class # Class
クラス再訪 - 本書P.47

クラスはオブジェクトで、クラス名は定数です。

定数

Ruby では大文字で始まる参照は、クラス名やモジュール名も含めて、すべて定数である。定数のスコープには、変数のスコープとは異なるルールがある。

定数 - 本書P.49

定数はディレクトリのようにツリー上にならんでいて、モジュールを使うことで MyModule::MyClass::MyConst のように書くことができるようになります。このように使うモジュールのことをネームスペースと呼んでいます。

既存のクラスやモジュール名と同名の参照を定義するとオープンクラスになってしまうので、ネームスペースを使って自分のクラスを判別できるようにすると行儀の良いプログラムになります。

メソッド探索

メソッドを呼び出すことは、レシーバに対してメッセージを送ることと言われます。レシーバとは、メソッドを呼び出したオブジェクトのことです。Ruby はメソッドを呼び出すと次の二つのことを行ないます。

  1. 呼び出すメソッドを探す。これをメソッド探索と呼びます。
  2. メソッドを呼び出す。これには self が必要になります。

Ruby はメソッドを呼び出す際に、レシーバの継承チェーンをたどってメソッドを見つけ出します。継承チェーンをたどる、とはクラスの親クラスを上にたどっていくことです。

モジュールはクラスをラップする

モジュールをクラスに(あるいは別のモジュールに)インクルードするとき、Ruby はちょっとしたトリックを使う。無名クラスを作ってモジュールをラップし、継承チェーンに挿入するのだ。それはインクルードするクラスの真上に入る。

module M
def my_method
"M#my_method()"
end
end

class C
include M
end

class D < C; end

D.ancestors # => [D, C, M, Object, Kernel, BasicObject]
モジュールとメソッド探索 - 本書P.60

Ruby には print() のようなどこからでも呼び出せるメソッドがあります。これは、Kernel モジュールのプライベートメソッドとして定義されているからです。

Ruby はすべてのクラスのスーパークラスとして Object クラスをセットします。この Object クラスの中で Kernel モジュールがインクルードされているため、すべてのクラスの中で Kernel メソッドのプライベートメソッドが呼び出せるのです。

これを利用すると、Kernel モジュールを再オープンしてメソッドを追加すれば、どこからでも呼び出せるメソッドが定義できます。

self

Ruby のコードはオブジェクト(カレントオブジェクト)の内部で実行される。カレントオブジェクトは self とも呼ばれる。self キーワードでアクセスできるからだ。

self の役割を担うオブジェクトは同時には複数存在しない。長時間その役割を担うオブジェクトも存在しない。メソッドを呼び出すときは、レシーバが self になる。その時点で、すべてのインスタンス変数は self のインスタンス変数になる。レシーバを明示せずにメソッドを呼び出すと、すべて self に対するメソッド呼び出しになる。他のオブジェクトを明示してメソッドを呼び出すと、そのオブジェクトが self になる。

self の発見 - 本書P.63

メソッドを呼び出さない時の self 、つまりトップレベルコンテキスト(スクリプトが実行されたときの一番最初のスコープ)では、self は main という Ruby インタプリタが作成したオブジェクトの内部にいます。

private メソッドのルール

Ruby の private メソッドは他の言語と少しルールが違います。Ruby の private メソッドは次の二つのルールにしたがって実行されます。

  1. Ruby は、自分以外のオブジェクトのメソッドを呼び出すには、レシーバを明示的に指定する必要がある。
  2. private のついたメソッドを呼び出すときは、レシーバを指定できない。

つまり、private メソッドを呼び出すには、自分自身(self)にしかできないことになります。

厳密には、Ruby1.9 以前は send メソッド, Ruby1.9 からは instance_eval を使えば呼び出すことができます。

class MyClass
  private
  def hoge
    "private called"
  end
end
 
obj = MyClass.new
obj.send(:hoge)                 # <-- Ruby1.8
obj.instance_eval {|o| hoge }   # <-- Ruby1.9

動的ディスパッチ

Ruby は、send() メソッドを使って呼び出したいメソッドをコードの実行時に決めることができます。これを 動的ディスパッチと呼びます。send() メソッドにはシンボルを渡すことが適当とされています。

シンボルと文字列の使い分けは、基本的には慣習なのである。多くの場合、シンボルは「物」の名前に使う。特にメタプログラミングに関するメソッドのような物である。

シンボル - 本書P.76

メソッドを動的に生成する

Ruby はメソッド呼び出しを動的に決めることができることの他に、メソッドを動的に定義することができます。メソッドを定義するには、Module#define_method() を使います。

class MyClass
  define_method :myMethod do |args|
    args * 5
  end
end

define_method をつかった例を本書から抜粋し次に示します。

メソッドを動的に生成する - 本書P.81

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end
 
  def self.define_component(name)
    define_method(name) {
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.to_s.capitalize}: #{info} ($#{price})"
      return "* #{result}" if price >= 100
      result
    }
  end
 
  define_component :mouse
  define_component :cpu
  define_component :keyboard
end

method_missing

Ruby はレシーバに対して呼び出されたメソッドを継承チェーンをたどって探します。この時、Object や BasicObject までたどってもメソッド定義が見つからない場合、Ruby インタプリタは method_missing を呼び出します。

動的ディスパッチでは呼び出すメソッド名を動的に作り出し、send() メソッドで呼び出していましたが、method_missing() をオーバーライドすることで、呼び出したいメソッドが定義されていない場合の動作を変更して、あたかもそこにメソッドがあるかのように振舞うことができます。

responde_to? のオーバーライド

method_missing の中で呼び出したメソッドは、ゴーストメソッド と呼ばれ、実際のメソッド定義はそこにはないものとして扱われます。しかし、これでは、Object#methods の一覧に含まれません。そこで、ゴーストメソッドがあたかもそこに本当にあるかのように振舞うように、respond_to? をオーバーライドするようにします。

class Computer
  def respond_to?(method)
    @data_source.respond_to?("get_#{method}_info") || super
  end
  #...
end

ブランクスレート

動的ディスパッチやゴーストメソッドであたかもそこにメソッドがあるかのように振舞うことができますが、同名のメソッドが既存のクラスに存在するような場合は、既存のクラスのメソッドが優先的に呼び出されてしまいます。せっかく動的ディスパッチやゴーストメソッドを意図していたのに、同名のメソッドがあることによりこの前提が崩れてしまうような場合、Module#undef_method() または、Module#remove_method() を使って既存のメソッドを削除することができます。

必要なメソッド以外何も無いことを、ブランクスレート(blank slate)、白紙の状態といいます。Ruby1.9 では BasicObject を継承することでブランクスレートなクラスをつくることができます。

Module#undef_method と Module#remove_method

undef_method は継承したメソッドも含めてすべてのメソッドを削除します。remove_method はレシーバのメソッドは削除しますが、継承したメソッドはそのまま残ります。

__で始まるメソッドは削除してはいけない

Ruby には で始まるメソッドが用意されています。例えば、send() メソッドに対して send() メソッド、id() メソッドに対して id() メソッドです。この で始まるメソッドは Ruby 内部から使われるメソッドとして予約されているため、undef_method や remove_method で削除するべきではありません。

スコープ

ブロック

ブロックは単体では実行できない。コードを実行するには、ローカル変数、インスタンス変数、self といった環境が必要になる。これらはオブジェクトにひもづけられた名前のことで、束縛とも呼ばれる。ブロックとは、これらをまとめて実行するためのものである。ブロックはコードと束縛の集まりで構成される。

クロージャ - 本書P.110

ブロックを定義すると、そのとき、その場所にある束縛(binding)を取得できます。ブロックを実行する時、この束縛も一緒にメソッドに渡されます。これにより、メソッドを実行するスコープでブロックを呼び出す変数を宣言することや設定することができるようになります。

ブロックの変数スコープの例

下の例では、メソッド定義の中で変数 msg を定義していますがメソッド定義の内部の msg は使われません。メソッドの呼び出し時にブロックを定義した際に束縛されたローカル変数 msg が使われます。

def greet
  msg = "Hi "
  p yield("hamasyou")
end
 
msg = "Hello"
greet {|name| "#{msg} #{name}" }

ローカル変数のスコープ

Ruby には Java や C# のような内部のスコープから外部のスコープの変数を参照できる入れ子は存在しません。外部のスコープと内部のスコープは区別されています。

変数の束縛が切り替わる例

v1 = 1
 
class MyClass
  v2 = 2
  local_variables
 
  def my_method
    v3 = 3
    local_variables
  end
 
  local_variables
end
# => [:v2]
obj = MyClass.new
obj.my_method    # => [:v3]
obj.my_method    # => [:v3]
local_variables  # => [:obj, :v1, :_]

トップレベルコンテキストで定義した v1 変数が MyClass の中で見えなくなっています。また、MyClass のクラススコープで定義した v2 も、my_method の中で見えなくなっています。

スコープゲート

プログラムがスコープを切り替える場所は次の3つあります。

  • クラス定義(class
  • モジュール定義(module
  • メソッド呼び出し(def

これらのスコープゲートを超えて束縛をわたすには、キーワードではなくメソッドを使います。

class => Class.new
module => Module.new
def => Module#define_method()
my_var = "Hello"
 
MyClass = Class.new do
  p "#{my_var}"
 
  define_method :my_method do
    p "#{my_var}"
  end
end

レキシカルスコープを入れ子で参照できるようにすることを、フラットスコープと呼びます。

instance_eval()

Object#instance_eval() を使うと、オブジェクトのコンテキストでブロックを評価することができます。instance_eval() はフラットスコープで評価されるので、ローカル変数にアクセスすることができます。

my_var = 13
obj.instance_eval { @v = my_var }
obj.instance_eval { @v }
# => 13

クラス定義

メソッド定義の意味

Ruby のプログラムは常にカレントオブジェクト self と、カレントクラスを持っています。そして、メソッドを定義すると、カレントクラスのインスタンスメソッドになります。

プログラム中で、カレントオブジェクトを参照するには self を使いますが、カレントクラスを参照するキーワードはありません。

では、どうやってカレントクラスを判断すればよいかというと、class キーワードを見ればよいです。(目視で確認ですw)

class でオープンされたカレントクラスにメソッドを定義すると、そのカレントクラスのインスタンスメソッドを定義することになります。

カレントクラス

既存のクラスのコンテキストでブロックを評価する Object#class_eval というメソッドがあります。これを使ってクラスのオープンをすることもできます。

class キーワードを使ってクラスをオープンするにはクラス名が必要ですが、class_eval() は変数としてクラスを受け取った場合にも使うことができます。

class MyClass; end
 
def add_hello_to(a_class)
  a_class.class_eval do
    def hello; "Hello" end
  end
end
 
add_hello_to MyClass
obj = MyClass.new
obj.hello
# => "Hello"

class_eval を使うと、selfカレントクラスに変更が加えられます。instance_eval は self のみに変更を加えます。

instance_eval()class_eval() はどちらを使う?

「このオブジェクトをオープンしたいが、クラスのことは気にしない」ならば、instance_eval() がいい。「ここでオープンクラスを使いたい」ならば、class_eval() がいい。

instance_eval() と class_eval() はどちらを使う? - 本書P.143

クラスインスタンス変数とクラス変数

クラスインスタンス変数とは、クラスのインスタンス変数のことでクラスに属しています。

class MyClass
  @my_var = 1
end

クラスインスタンス変数は、クラスのオブジェクトのインスタンス変数とは別物です。

class MyClass
  @v = 1
  def my_method
    @v = 2
  end
  def self.my_method
    @v
  end
end
 
MyClass.my_method    # => 1
obj = MyClass.new
obj.my_method    # => 2
MyClass.my_method    # => 1

クラス変数とは、@@プレフィックスを付けた変数で、クラスの階層に属しています。

class C
  @@v = 1
end
 
class D < C
  def my_method
    @@v
  end
end
 
D.new.my_method    # => 1

クラス変数は、サブクラスからもアクセスできます。

特異メソッド

Ruby では、特定のオブジェクトにのみメソッドを追加することができます。これを特異メソッドと呼びます。

msg = "hello, my name is hamasyou"
 
def msg.hello
  self.upcase
end
 
msg.hello    # => "HELLO, MY NAME IS HAMASYOU"

特異クラスのスコープ

Ruby には class キーワードを使った特別な構文があり、これを使うことで特異クラスのスコープに入ることができます。

class << an_object
  # 特異スコープの中
end

クラスメソッドの構文

クラスメソッドは実は、クラスの特異メソッドとなっています。つまり、クラスメソッドを定義するには、次のいずれかの方法を使うことができます。

class MyClass
  def self.my_method; end
end
 
def MyClass.my_method; end
 
class MyClass
  class << self
    def my_method; end
  end
end

インスタンスに特異メソッドを動的に追加するには特異クラスにメソッドを定義する

メソッドはクラスの持ち物なので、特異メソッドを動的にインスタンスに定義するにはインスタンスの特異クラスに対してメソッドを定義してやる必要があります。

特異メソッドを動的に定義するのに、instance_eval() メソッドを使うことができます。

じつは、instance_eval() メソッドを使うとレシーバのカレントクラスを特異クラスに変更しているのです。instance_eval() メソッドを使って特異クラスをオープンすることで、特異クラスに対してメソッドを定義することができるようになります。

class MyClass
end
 
obj = MyClass.new
 
obj.instance_eval("def hello; 'Hello World'; end")
obj.hello
# => "Hello World"
 
obj2 = MyClass.new
obj2.hello
# => NoMethodError: undefined method `hello' for #<MyClass:0x0xxxxx>

または、特異クラスのオブジェクトを class << obj 構文を使って取得し、そのオブジェクトに対して define_method でメソッドを定義します。

class MyClass
end
 
obj = MyClass.new
singleton = class << obj; self; end
singleton.send(:define_method, :hello) { "Hello World" }
 
obj.hello
# => "Hello World"

モジュールをインクルードしてクラスメソッドを定義する

「来る日も来る日も世界のどこかで、Rubyプログラマがモジュールをインクルードしてクラスメソッドを定義しょうとしている。私もかつてはそうだった。しかし、これではうまくいかない。」

モジュールの不具合 - 本書P.167

モジュールの中にクラスメソッドを定義して、モジュールをインクルードしたクラスにだけクラスメソッドを定義したいという場合、下のようにしがちですがこれは間違った例です。

モジュールをインクルードしてクラスメソッドを定義する間違った例

module MyModule
  def self.my_method; "Hello"; end
end
 
class MyClass
  include MyModule
end
 
MyClass.my_method
# => NoMethodError: undefined method `my_method' for MyClass:Class

モジュールをインクルードすると、モジュールのインスタンスメソッドが手に入ります。クラスメソッドではありません。すなわち、モジュールをインクルードしてクラスメソッドを手に入れるためには、モジュールにはインスタンスメソッドとして定義して、include する場所をクラスメソッドが定義される場所(つまり、特異クラス)で行うようにします。

module MyModule
  def my_method; "Hello"; end
end
 
class MyClass
  class << self
    include MyModule
  end
end
 
MyClass.my_method    # => "Hello"

クラスメソッドは特異クラスのメソッドなので、特異クラスにメソッドを定義すればクラスメソッドになります。

class MyClass
  class << self
    def my_method; end
  end
end

モジュールを特異クラスのメソッドに追加する方法、モジュールをオブジェクトに追加して特異メソッドとする方法は、Ruby がそのためのメソッドを用意しています。それが Object#extend() です。

module MyModule
  def my_method; "Hello"; end
end
 
obj = Object.new
obj.extend MyModule
obj.my_method
# => "Hello"
class MyClass
  extend MyModule
end
 
MyClass.my_method    # => "Hello"

エイリアス

alias キーワードを使うと、Ruby のメソッドにエイリアスをつけることができます。

alias :new_method :old_method

alias された時点で、Ruby は古いメソッドを新しいメソッドとして再定義(コピー)します。したがって、次のような動作となります。

class String
  alias :real_length :length
 
  def length
    real_length > 5 ? "long" : "short"
  end
end
 
"War and Peace".length    # => "long"
"War and Peace".real_length    # => 13

オープンクラスを使って、length() メソッドを再定義していますが、alias を使って real_length として再定義(コピー)しているので、real_length を呼び出すことで元の動作を呼び出すことができています。

Kernel#eval

eval() メソッドは引数で指定されたコード文字列(Ruby のコード)を処理します。

array = [10, 20]
element = 30
eval("array << element")  # => [10, 20, 30]

eval()、instance_eval()、class_eval() に渡すコード文字列の中は、ローカル変数にアクセスできます。

array = ["a", "b", "c"]
x = "x"
array.instance_eval "self[1] = x"
array    # => ["a", "x", "c"]

Binding オブジェクト

Binding はスコープをオブジェクトにまとめたものです。eval()、instance_eval()、class_eval() に Binding を渡すことでそのスコープでコードを実行できます。

class MyClass
  def my_method
    @x = 1
    binding
  end
end
 
b = MyClass.new.my_method
eval "@x", b    # => 1

フックメソッド

Ruby には様々なイベントへのフックメソッドが用意されています。よく使われるのが、Module#included() で、モジュールが include された時に呼び出されます。次のようにすることで、Module で定義されたインスタンスメソッドをクラスメソッドに追加することができます。

module MyMixin
  def self.included(base)
    base.extend(MyClassMethods)
  end
 
  module MyClassMethods
    def hoge
      "hoge()"
    end
  end
end
 
class MyClass
  include MyMixin
end
 
MyClass.hoge    # => "hoge()"