LoginSignup
10
7

More than 5 years have passed since last update.

Ruby: method_missing を使ってみた

Last updated at Posted at 2015-01-07

はじめに

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

references.png

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7