こんにちは、鈴木です。
前回読んだ Hash#reverse_merge は、着目している変数をレシーバにできる便利なメソッドでした。
core_ext/object/inclusion.rb で定義されている Object#in? も同様に、着目している変数をレシーバにできるメソッドです。
そのうちリファクタリングのパターンの一つに「着目している変数をレシーバにする」が加わる日が来るかもしれませんね(^^;
Object#in?
まずはソースコードを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
def in?(*args) if args.length > 1 args.include? self else another_object = args.first if another_object.respond_to? :include? another_object.include? self else raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?' end end end |
単純な実装かと思いきや、いくつか条件分岐が行われています。
引数が複数の場合
一番簡単な部分は、最初の「if args.length > 1」が true となった場合の処理ですね。
1 2 3 4 |
if args.length > 1 args.include? self else ... |
Object#in? は可変長引数を取りますが、複数の引数が渡された場合にここを通ります。
つまり、以下のような呼び出しに対して、「args.include? self」が実行されます。
1 |
1.in?(1, 2, 3) |
引数が 1 つの場合
それでは最初の条件分岐「if args.length > 1」が false となった場合、つまり引数が 1 つ(以下)の場合に実行されるコードを見てみましょう。
1 2 3 4 5 6 |
another_object = args.first if another_object.respond_to? :include? another_object.include? self else raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?' end |
この条件分岐には、以下のような呼び出しが行われた時に通ります。
1 |
1.in?([1, 2, 3]) |
「another_object = args.first」では args の先頭要素を取得しています。
「1.in?([1, 2, 3])」という呼び出しが行われると、args は「[[1, 2, 3,]]」となりますので、そこから「[1, 2, 3]」を取得しているわけですね。
その後の処理は、取り出した another_object が include? メソッドを持つかどうかで分岐します。
「if another_object.respond_to? :include?」の部分が判定部分です(respond_to? はレシーバが指定された名前のメソッドを持つかどうかを調べるメソッドです)。
another_object が include? メソッドを持つ場合は「another_object.include? self」が実行され、そうでなければ ArgumentError が raise されます。
Hash に対する in? はキーに含まれるか判定する
Object#in? の引数にハッシュを指定した場合の動作を確認しましょう。
1 2 3 |
hash = {1 => :one, 2 => :two, 3 => :three} p 1.in?(hash) # => true p :one.in?(hash) # => false |
このように、Object#in? の引数に Hash を指定した場合は、Hash のキーに含まれるかどうか判定されます。
これは、「1.in?(hash)」とすると「hash.include?(1)」が呼び出されますが、Hash#include? はキーに含まれるかどうかを判定するためです。
「Hashの値に含まれるかどうか」ではないので注意しましょう。
Hash の値に含まれるかどうか判定するメソッドを作る
Hash の「キー」ではなく「値」に含まれるかどうか判定したい場合はどうしましょうか。
Hash#value? メソッドで実現できるのですが、Object#in? のように着目している変数をレシーバにしたいです。
Object#in? のコードを読んだ今なら、それを実現するコードを書くことは簡単なはずです。
以下のようになります。
1 2 3 4 5 6 7 8 9 |
class Object def in_values?(hash) hash.value?(self) end end hash = {1 => :one, 2 => :two, 3 => :three} p 1.in_values?(hash) # => false p :one.in_values?(hash) # => true |
Ruby2.0 の Refinements を使ってみる
ここで我に返ると、目先の便利さと引き換えに Object クラスを拡張してしまいました。
Object クラスを拡張すること自体は決して悪いことではありませんが、プログラム全体に影響する拡張であることを忘れてはいけません。
既存クラスの拡張は、拡張することの影響と、もたらされるメリットとのトレードオフです。バランス感覚が大切です。
便利な拡張をしたいが、影響は限定的にしたい! という場合に思いつくのは、Ruby2.0 の Refinements (実験的機能)です。
Refinements を使うと、以下のように実現することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module HashInValuePredicate refine Object do def in_values?(hash) hash.has_value?(self) end end end using HashInValuePredicate hash = {1 => :one, 2 => :two, 3 => :three} p 1.in_values?(hash) # => false p :one.in_values?(hash) # => true |
まとめ
ActiveSupport が提供する Object#in? のコードを読むという趣旨で書き始めたのですが、少し発散してしまいました・・(^^;
書いているうちに「コアクラスを拡張するときのトレードオフ」を考え始めてしまい、そういえば Ruby2.0 からは Refinements が実験的機能として登場するな、なとど色々考えてしまいました。
ところで、少し昔のバージョンの ActiveSupport では「require 'active_support'」と書くと、全てのファイルが読み込まれていました。
それが、最近では「require 'active_support/core_ext/object/inclusion'」のように個別に読み込むことができるようになりました。まるごと読み込む場合は「require 'active_support/all'」です。
これが ActiveSupport のバランス感覚なんでしょうね。