[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でもジャンプしようがありません。

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

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833 コボラー、ITコンサル、ローカライズ業界を経てなぜかWeb開発者志願。 これまでにRuby on Rails チュートリアルの大半、Railsガイドのほぼすべてを翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ