- Ruby / Rails関連
[Rails5] Active Support Core ExtensionsのString#inquiryでメタプログラミング
こんにちは、hachi8833です。Active Support探訪シリーズ、今回はString#inquiryにお邪魔します。実用的なコードとしてはおそらく最もシンプルなメタプログラミングを見つけたのでそのあたりを読み進めてみました。
今回のメソッド
- メソッド:
String#inquiry - ディレクトリ配置: https://github.com/rails/rails/blob/5-0-stable/activesupport/lib/active_support/core_ext/string/inquiry.rb
String#inquiry実は地味ながら便利なメソッドです。以下のようなRails.envでの使い方が典型的です。
# Inquiryを使わない場合
Rails.env == 'production'
# Inquiryを使った場合
Rails.env.production?
条件にリテラルを直に書き込む== "production"よりも、リテラルを使わずにtrue/falseをかっこよくチェックできる.production?の方がRailsらしい書き方とされています。
Railsガイドにも説明がありますが、以下の公式サンプルがわかりやすいですね。
pets = [:cat, :dog].inquiry
pets.cat? # => true
pets.ferret? # => false
pets.any?(:cat, :ferret) # => true
pets.any?(:ferret, :alligator) # => false
条件
- Railsバージョン: 5-0-stable
- Rubyバージョン: 2.4.0
String#inquiry
inquiryは以下のようなシンプルなコードです。例によってコメントなどは省略しています。
require 'active_support/string_inquirer'
class String
def inquiry
ActiveSupport::StringInquirer.new(self)
end
end
ActiveSupport::StringInquirerの実体は、active_support/string_inquirer.rbにありました。今回はすぐにエンドが見えてほっとしました。
module ActiveSupport
class StringInquirer < String
private
def respond_to_missing?(method_name, include_private = false)
method_name[-1] == '?'
end
def method_missing(method_name, *arguments)
if method_name[-1] == '?'
self == method_name[0..-2]
else
super
end
end
end
end
これはいわゆるメタプログラミングというやつですね。実はメタプロのコードをみっちり読むのはこれが初めてですが、これならなんとかなりそうです。
method_missingの内容
以下のmethod_missingはRubyのBasicObject#method_missingをオーバーライドしていることになります。
def method_missing(method_name, *arguments)
if method_name[-1] == '?'
self == method_name[0..-2]
else
super
end
end
method_name[-1]はメソッド名末尾を、method_name[0..-2]はメソッド名の末尾以外をそれぞれ指します。メソッド名末尾が?で終わっていれば自身が持つ文字列とメソッド名本体が一致するかどうかをtrue/falseで返します。
それ以外のメソッドはsuperで見送りますので、StringやObjectで通常どおり処理されます。
respond_to_missingの役割
ところで、上のmethod_missingオーバーライドがあるので完成かと思いきや、以下のrespond_to_missingメソッドもオーバーライドしています。これは一体何をしているのでしょうか。
def respond_to_missing?(method_name, include_private = false)
method_name[-1] == '?'
end
これはリファレンスマニュアル: Object#respond_to_missing?を読むとわかります。
Object#respond_to? はメソッドが定義されていない場合、 デフォルトでこのメソッドを呼びだし問合せます。BasicObject#method_missing を override した場合にこのメソッドも override されるべきです。
リファレンスマニュアル: Object#respond_to_missing?より
Object#respond_to?が期待どおりに動くよう、respond_to_missingを適切にオーバーライドしないとしないといけないんですね。メタプロするときはこれを忘れないようにしましょう。
メタプログラミングに触れて
ご存知のとおり、メタプログラミングという手法はRailsのさまざまなコード、特にActive Recordで激しく使われています。
末尾が?で終わるメソッド名であれば、Stringクラスが元々持っているメソッドであってもmethod_missingでキャッチしてしまうのかなと思っていましたが、クラスが持っているメソッドがあればそもそもmethod_missingが動き出すこともありませんね。
これを応用すれば、メソッドをわざわざ書かなくてもメソッドが動的に生えてくるかのようにクラスが振る舞うわけです。実際はメソッド名に応答するようになっただけといえばそれまでですが。
実際にはそのメソッド名はないので、IDEのコードジャンプで追いたくてもメソッド名がインデックス化されるはずもありません。method_missingもrespond_to_missing?も通常のメソッド呼び出しとまったく経路が違うので、RubyMineでもジャンプしようがありません。
メタプログラミングは非常に強力である反面、やりすぎるとコードを追うのが大変困難になるのは私のような者でも想像がつきます。
