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

[Rails5] Active Support Core ExtensionsのString#acts_like_string?

こんにちは、hachi8833です。Active Support探訪シリーズ、今回はString#acts_like_string?にお邪魔します。

今回のメソッド

条件

string/behavior.rb

class String
  # Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
  def acts_like_string?
    true
  end
end

拍子抜けするような簡素なメソッドです。コメントによれば、Stringと似た振る舞いをするクラスで、duck-typingがより期待どおりに動作するためのメソッドであるとのことです。

Object#acts_like?を見てみましょう。devdocs.ioで探すのが早いですね。

object/acts_like.rb

class Object
  # A duck-type assistant method. For example, Active Support extends Date
  # to define an <tt>acts_like_date?</tt> method, and extends Time to define
  # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
  # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
  # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
  def acts_like?(duck)
    respond_to? :"acts_like_#{duck}?"
  end
end

大意: ダックタイピングのアシスタントメソッドです。
例: Active SupportではDateをextendして#acts_like_date?メソッドを定義し、Timeをextendして#acts_like_time?を定義しています。これにより、x.acts_like?(:time)<tt>x.acts_like?(:date)などのようなダックタイプ安全かどうかの比較を行えます。Timeとして振る舞って欲しいクラスにはacts_like_time?メソッドが定義されていて欲しいからです。

String#acts_like_string?はダックタイピングのためだったんですね。開発者が直接使うというより、Rails内部で使われることが多いのではないかととりあえず推測しました。

String#acts_like_string?を使ったダックタイピングがRailsの中でどのように使われているのか、ユースケースを掘り出してみましょう。

Railsでの用例

GitHubで検索してみると、activerecord/lib/active_record/sanitization.rbに一箇所だけ用例がありました。

コメントや途中のコードは適宜...で省略しています。

module ActiveRecord
  module Sanitization
    extend ActiveSupport::Concern
    module ClassMethods
      protected
        # Accepts an array or string of SQL conditions and sanitizes
        # them into a valid SQL fragment for a WHERE clause.
        #
        #   sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
        #   # => "name='foo''bar' and group_id=4"
        ...
        alias :sanitize_sql :sanitize_sql_for_conditions
        alias :sanitize_conditions :sanitize_sql
        ...
        def replace_bind_variable(value, c = connection) # :nodoc:
          if ActiveRecord::Relation === value
            value.to_sql
          else
            quote_bound_value(value, c)
          end
        end
        ...
        def quote_bound_value(value, c = connection) # :nodoc:
          if value.respond_to?(:map) && !value.acts_like?(:string)
            if value.respond_to?(:empty?) && value.empty?
              c.quote(nil)
            else
              value.map { |v| c.quote(v) }.join(",")
            end
          else
            c.quote(value)
          end
        end
        ...
    end
    ...
  end
end

いつの間にかActive Recordのprotectedなモジュールにまで足を踏み入れてしまいました。

大意:
# SQL条件の配列または文字列を受け取り、サニタイズして
# WHERE句で使える有効なSQLフラグメントにする
#
# sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"

#sanitize_sql_for_conditions->#sanitize_sql_array->#replace_bind_variable->#quote_bound_valueが呼び出され、その中で以下の文があります。

          if value.respond_to?(:map) && !value.acts_like?(:string)

文字列でないかどうかの判定に使っているようですが、正直これだけだとありがたみがピンときませんでした。

acts_like?の使い道

acts_like?はどんなふうに使うことが想定されているのでしょうか。

Object#acts_like?(ActiveSupport)という記事を見かけたので、以下に引用します。

  • 個人的には、respond_to?で単にあるメソッドがあるか調べるのに比べて、コードの意図が明確になると思います。
    Object#acts_like?(ActiveSupport)より

当初はRails内部での利用が主なのかなと推測しましたが、自分のクラスにacts_like?を独自に実装して、同じ流儀で判定できるようにすることが想定されているのかもしれないと思いました。

参考

morimorihogeさんのサジェスチョン

acts_like?の利用法について、BPS Webチーム部長のmorimorihogeの知恵をお借りしましたので自分なりに再構成しました。以下はコードからわかる範囲での見解に基づいていますのでご了承ください。


#acts_like?については、インターフェースのチェックという意味合いが考えられます。たとえば#respond_to?だと個別のメソッドの存在確認しかできないので、StringならStringとしてのインターフェース集合があるかどうかをチェックするのによさそうです。

メソッドの存在確認を#respond_to?で行おうとすると、たとえば#performのような具体的でないメソッド名はクラスによって動作が異なってしまう可能性があるので、振る舞いまでは#respond_to?などでは調べきれません。

ひとつの方法ですが、(Railsではなく)RubyのModule#include?を使ってモジュールの有無をチェックする方がより確実かもしれません。

module A
end
class B
  include A
end
class C < B
end
B.include?(A)   #=> true
C.include?(A)   #=> true
A.include?(A)   #=> false

ただし、モジュールという概念に収まりきれないような横断的な機能についてインターフェースをチェックするのであれば、#acts_like?のような方法が役に立つこともあるかもしれません。

なお、behavior.rbのblameは2008-2009年とだいぶ昔です。当時のプルリクbabbc15@ManfredFixを見ると、先ほどの#quote_bound_valueはもともとactiverecord/lib/active_record/base.rbにあったんですね。

quote_bound_value

おまけ

関連記事

[Rails5] Active Support Core ExtensionsのStringクラス(1)String#blank?

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


CONTACT

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