View on GitHub

akkunchoi.github.com

Ruby メタプログラミング (1. クラス)

書籍「メタプログラミングRuby」を参考に、メタプログラミングの方法を簡単にまとめます。

文法

書籍「メタプログラミングRuby」には文法に関する説明はありません。 クラスに関する文法は http://doc.ruby-lang.org/ja/1.9.3/doc/spec=2fdef.html を参考にして下さい。

オープンクラス

Rubyは既存のクラスを拡張することができます。

# Stringクラスを拡張する
class String
  def to_alnum
    gsub /[^\w\s]/, ''
  end
end

ただし、それができるとはいえ、やるべきかどうかはまた別。

  • やるべきかどうかの基準
    • (Stringの例の場合)すべての文字列が持っていても問題ない汎用的な機能だろうか?
    • 特定のドメインに特化したメソッドを追加しようとしてないだろうか?
  • 他の方法には…
    • 別の AlphanumericString クラスを定義する
    • 特異メソッドを使って、特定の String インスタンスにだけメソッドを追加する。

再オープン

クラス定義とその他のコードには違いがありません。

クラス定義も式です。classにもきちんと戻り値があります。

# classの戻り値
class A
  'hoge'
end
=> "hoge"

同じクラスに対して何度もclassで呼ぶことができます。

# クラスCを3回定義している?
3.times do
  class C
    puts "Hello"
  end
end
# Hello
# Hello
# Hello

この場合、定義を繰り返しているのではなく、

  • クラスが存在していない時は、クラスを定義する。メソッドがあればメソッドも定義する。
  • クラスが存在している時は、既存のクラスを再オープンして、メソッドを追加する。

という動作を行います。

class キーワードはクラス宣言というより、スコープ演算子のようなもの。

既存のクラスを再オープンして、いつでもその場で修正できることをオープンクラスと呼びます。

オープンクラスの問題点: モンキーパッチ

オープンクラスはすでに用意されているメソッドを簡単に書き換えることができます。 そのため、知らぬ間に依存している機能の動作を変更することで、バグにつながりやすい。

クラスへの安易なパッチを蔑称してモンキーパッチと呼びます。

モンキーパッチは「意図せずに起きる」のと「意図的に適用」する場合があります。

便利だけど危険がつきまとうので、 独自のメソッドを定義する前に、クラスに既存のメソッドがないかを注意深く確認することが大事です。

インスタンス変数とインスタンスメソッド

インスタンス変数はハッシュのキー・バリューのようなもの。セットして初めて存在します。 インスタンス変数はオブジェクトに、インスタンスメソッドはクラスに住んでいます。

# 
class MyClass
  def my_method
    @v = 1
  end
end
obj = MyClass.new
obj.instance_variables # [@v]
obj.methods            # ["my_method"]

クラスはオブジェクト

(もう少し説明足したい)

クラスは Class クラスのインスタンスです。

# classメソッド
"hello".class # => String
String.class  # => Class
Class.class   # => Class

Classはnew, allocate, superclassを追加したモジュールに過ぎない!

# Classに定義されているインスタンスメソッド
inherited = false
Class.instance_methods(inherited) # => [:superclass, :allocate, :new]

Stringのクラス階層

# Stringのクラス階層
String.superclass     # => Object
Object.superclass     # => BasicObject
BasicObjct.superclass # => nil

Classのクラス階層

# Classのクラス階層
Class.superclass      # => Module
Module.superclass     # => Object
Object.superclass     # => BasicObject
BasicObjct.superclass # => nil

クラス階層とインスタンスの関係をまとめてみる。 (<==== は is_a 関係、<----はクラスの親子関係)

               BasicObject
                    |
                    v 
     Class <====  Object
                    |
                    |--> String   <==== "Hello"
                    |
                    |--> MyClass  <==== myObj (= MyClass.new)
                    |
                    `--> Module
                         |
                         `--> Class

# もちろん Object だけでなく BasicObject, String, MyClass, Module, Class も Class のインスタンス

定数

大文字で始まる参照は、クラス名やモジュール名も含めて、すべて定数になります。 定数はモジュールで階層化することができ、名前が同じでもパスが違えば違う定数になります。 ファイルシステムのような階層構造です。

# 定数
module A
  B = 'external'
  class C
    B = 'internal'
  end
end

A::B    # => 'external'
A::C::B # => 'internal'

パスの指定の方法もファイルシステムみたい。

# 定数のパス
module A
  B = 'external'
  class C
    B = 'internal'
    # コロン2つで始めれば、絶対パスで指定できる
    ::A::B # => 'external'
  end
  # 内部に書けば相対パスで指定できる
  C::B  # => 'internal'
end

constants()メソッドでモジュールに定義されている定数を取得できます。

# 定数の一覧
A.constants         # => [:B, :C]
Module.constants    # => [:Object, :Module, ...]

定数は警告が出るけど、上書きできます。

# 定数の上書き
module M
end
module N
end
M = N # warning: already initialized constant M
M     # => N

load

# load
load('foo.rb')        # foo.rbで定義された定数が読み込まれる
load('foo.rb', true)  # 無名モジュールを作成して、foo.rbを取り込むので汚染しない
  • load はコードを実行するために使う。
  • require はライブラリを読み込むために使う。

その他クラスに関して

Rubyのクラスは慣習としてキャメルケースを使います。

  • NG => TEXT
  • OK => Text

先にmoduleが定義されてあればclassは定義できません。 クラスだったらオープンクラスで上書きできるけど。

# TypeError
module Text
end
class Text # Text is not a class
end

例えばActionMailerをロードしたら Text モジュールが定義されてしまう。 これを避けるには、別のネームスペース(module)を作ります。

# 
module Bookworm
  class Text
    # ...
  end
end