Edited at

Ruby: method_missing を使ってみた

More than 3 years have passed since last update.


はじめに

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

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 で定義されています。

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

class Object

# sym は呼ばれたメソッドの名前 (Symbol)、* はその引数
private
def method_missing(sym, *)
# 呼ばれたメソッドの名前が `array?` の場合はフックする
return kind_of?(Array) if sym == :array?

super # 基底クラスの処理(本来の処理)を実行する
end
end

method_missing の一番目の引数が呼ばれたメソッドの名前です。

二番目以降の引数が呼ばれたメソッドの引数になります。上の例ではメソッド内で引数は見ないので「*」を指定しています。

フックしたい条件以外の場合は本来の処理を実行したいので、最後に super で基底クラスの処理を呼び出しています。

super の呼び出し時に後ろに何もつけない場合、メソッドに与えられた引数(ブロックも含め)を渡して基底クラスの同名メソッドを呼び出します。

  super                    # メソッドの引数をすべて渡す

super(1,2,3) # 任意の引数(ここでは 1,2,3)を渡す
super() # 引数を渡さない

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

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)は本稿の末尾に掲載します。

つくりは上で説明したのと同様です。

使ってみます。

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? だけ特別仕様にして再定義してみます。

class Array

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

def tuple?(n=2) # この実装は、おまけ
size == n
end
end

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


実装(参考)


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 可視性のメソッド)

上図の Graphviz 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?";
}