Ruby: method_missing を使ってみた

# はじめに

`obj.kind_of?(Array)` と同等のことをする `obj.array?`が欲しくなりました。

```rb
p [].array?           #=> true      # [].kind_of?(Array) と同じこと
p {}.array?           #=> false     # {}.kind_of?(Array) と同じこと
```

array? メソッドは、Array 以外のクラスも(false を返すために)持つ必要があります。
そこで、Object クラスを拡張することにしました。
どうせ、Object を拡張するなら`hash?`や`string?`なんかも作ろうと思いました。
ですが、全部のクラスに対応するメソッドをいちいち書いてられません。
なので、`method_missing` を使うことにしました。

# method_missing
`method_missing` はそのオブジェクトが持っていないメソッドを呼び出した時に、デフォルトで呼ばれるメソッドです。BasicObject で定義されています。

- [instance method BasicObject#method_missing (Ruby 2.0.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.0.0/method/BasicObject/i/method_missing.html)


method_missing を再定義することで、持っていないメソッドが呼ばれた時の処理をフックできます。

```rb
class Object
                   # sym は呼ばれたメソッドの名前 (Symbol)、* はその引数
  private
  def method_missing(sym, *)
                   # 呼ばれたメソッドの名前が `array?` の場合はフックする
    return kind_of?(Array) if sym == :array?
  
    super          # 基底クラスの処理(本来の処理)を実行する
  end
end
```

method_missing の一番目の引数が呼ばれたメソッドの名前です。
二番目以降の引数が呼ばれたメソッドの引数になります。上の例ではメソッド内で引数は見ないので「*」を指定しています。
フックしたい条件以外の場合は本来の処理を実行したいので、最後に `super` で基底クラスの処理を呼び出しています。
`super` の呼び出し時に後ろに何もつけない場合、メソッドに与えられた引数(ブロックも含め)を渡して基底クラスの同名メソッドを呼び出します。

```rb
  super                    # メソッドの引数をすべて渡す
  super(1,2,3)             # 任意の引数(ここでは 1,2,3)を渡す
  super()                  # 引数を渡さない
```

`array?` メソッドを持っているかのように振る舞うよう、`respond_to?` と`respond_to_missing?` も再定義します。

```rb
class Object
  def respond_to?(sym, *)
    (sym == :array?) || super     # メソッド名が `array?` の場合は true を返す、そうでない場合は基底クラスの処理を実行する
  end

  private
  def respond_to_missing?(sym, *)
    (sym == :array?) || super     # メソッド名が `array?` の場合は true を返す、そうでない場合は基底クラスの処理を実行する
  end
end
```

# hash?、string? なども実装する
上の例では、`array?` のみの対応になります。
`hash?`、`string?` など Array クラス以外にも対応させるように実装を変更します。

テストメソッドの名前は、対象のクラス名(upper-camelcase)を snakecase に変換（'`::`' は '`__`' に変換）して末尾に'`?`' をつけた名前にします。

```
# 対象クラス名とテストメソッド名の対応例

(対象クラス名)          (テストメソッド名)
Hash             <---> hash?
TrueClass        <---> true_class?
IO               <---> i_o?                # 'io?' ではありません
Enumerator::Lazy <---> enumerator__lazy?
ARGF.class       <---> _ARGF_class?        # ここは特例
```

具体的な実装(kind_of_test.rb)は本稿の末尾に掲載します。
つくりは上で説明したのと同様です。

使ってみます。

```rb
require 'kind_of_test'

class Object
  include KindOfTest
end

p [].array?               #=> true
p {}.array?               #=> false

p {}.hash?                #=> true
p true.true_class?        #=> true
p true.true?              #=> true

enum = (1..3).lazy.to_enum
p enum.enumerator__lazy?  #=> true

p ARGF._ARGF_class?       #=> true
p STDOUT.i_o?             #=> true

p 1.integer?              #=> true
p 1.fixnum?               #=> true
p 1.numeric?              #=> true
p 1.float?                #=> false

class MyClass ; end       # 任意のクラス
obj = MyClass.new
p obj.my_class?           #=> true
```

OK です。

---
# array? の特別仕様
ここで、Array#array? だけ特別仕様にして再定義してみます。

```rb
class Array
  def array?(&test)
    !test || all?(&test)  # ブロックが指定されている場合、all? での判定結果を返す
  end

  def tuple?(n=2)         # この実装は、おまけ
    size == n
  end
end
```
```rb
p [1,2,3].array?                   #=> true
p [1,2,3].array? &:integer?        #=> true    # 要素すべてが Integer の Array である 
p [1,2,3].array? &:string?         #=> false   # 要素すべてが String の Array ではない
p [1,2,3,:foo].array? &:integer?   #=> false   # 要素すべてが Integer の Array ではない
p [[1,2],[3,4]].array? &:tuple?    #=> true    # 要素すべてが 2要素の Array である
```

これで、以上です。
一番やりたかったことは最後の `array?` と要素のテストの組み合わせ表現でした。

#おわりに
本稿内容の動作確認は以下の環境で行っています。

- Ruby 2.1.5 p273



# 実装(参考)
```rb:kind_of_test.rb
module KindOfTest
  def true?  ; true_class?  ; end    # この実装は、おまけ
  def false? ; false_class? ; end    # この実装は、おまけ

  def respond_to?(sym, *)
    kind_of_test?(sym) || super
  end

  private
  def respond_to_missing?(sym, *)
    kind_of_test?(sym) || super
  end

  def method_missing(sym, *)
    return kind_of?(kind_of_test_class(sym)) if kind_of_test?(sym)
    super
  end

  def kind_of_test?(sym)
    !!kind_of_test_class_name(sym)
  end

  def kind_of_test_class(sym)
    eval kind_of_test_class_name(sym)
  end

  def kind_of_test_class_name(sym)
    return 'ARGF.class' if sym == :_ARGF_class?

    return unless sym =~ /\?\z/
    conv = -> s { s.split('_').map(&:capitalize).join }
    name = (sym.to_s.chop).split('__').map(&conv) * '::'
    name if (self.class.const_defined? name) && ((eval name).class == Class)
  end

  # ここから先は、おまけ仕様 (あると便利と思います)
  public
  def array?(&test)
    kind_of?(Array) && (!test || all?(&test))
  end

  def tuple?(n=2)
    kind_of?(Array) && (size == n)
  end

  def byte?
    kind_of?(Integer) && (0..255).include?(self)
  end
end
```

KindOfTest のメソッド依存関係図。(箱型は既存メソッド。二重線は public 可視性のメソッド)

![references.png](https://qiita-image-store.s3.amazonaws.com/0/60624/ce2e5731-beb8-c738-6968-42a3eacb18e4.png)

上図の Graphviz DOT形式ソース。

```dot:references.dot
digraph {
    rankdir=UD;
    "method_missing"		[shape = box];
	"respond_to_missing?"	[shape = box];
    "respond_to?"			[shape = box, peripheries = 2];
    "kind_of_test?"       -> "kind_of_test_class_name";
    "kind_of_test_class"  -> "kind_of_test_class_name";
    "method_missing"      -> "kind_of_test?";
    "method_missing"      -> "kind_of_test_class";
    "respond_to_missing?" -> "kind_of_test?";
    "respond_to?"         -> "kind_of_test?";
}
```