はじめに
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
実装(参考)
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形式ソース。
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?";
}