Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

[Rails5] Active Support Core ExtensionsのString#inquiryでメタプログラミング

こんにちは、hachi8833です。Active Support探訪シリーズ、今回はString#inquiryにお邪魔します。実用的なコードとしてはおそらく最もシンプルなメタプログラミングを見つけたのでそのあたりを読み進めてみました。

今回のメソッド

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

条件

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_missingrespond_to_missing?も通常のメソッド呼び出しとまったく経路が違うので、RubyMineでもジャンプしようがありません。

メタプログラミングは非常に強力である反面、やりすぎるとコードを追うのが大変困難になるのは私のような者でも想像がつきます。

関連記事

Rails: ActiveSupport::Inflectorの便利な活用形メソッド群

Rails: ビューのHTMLエスケープは#link_toなどのヘルパーメソッドで解除されることがある


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。