Techioz Blog

カスタム Ruby クラスを使用した Enumerable#grep

概要

私は Ruby 3.2 を使用しており、(デモンストレーションの目的で) 値の配列を格納するカスタム クラス Foo を持っています。

私の制御下にない別のコード部分があります。これは、Enumerable#grep と Enumerable#grep_v を使用して配列内の文字列パターンを検索します。

FooクラスをArray#grepで動作させたいと考えています。 https://docs.ruby-lang.org/en/3.2/Enumerable.html#method-i-grep を調べたところ、次のように述べられています

このように #=== メソッドを実装してみました

class Foo
  def initialize(values)
    @values = values
  end

  def ===(pattern)
    @values.any? { |v| v.match?(pattern) }
  end
end

しかし、これはうまくいかないようです:

[Foo.new(["foo", "bar"]), Foo.new(["zoo"])].grep(/oo/)
=> []

また、デバッガーを配置するか、def ===(pattern) 内にステートメントを置くと、それが grep コマンドによって呼び出されないようであることがわかります。

これがどのように機能するかについて何かアイデアはありますか?

解決策

Enumerable#grep のドキュメントをよく見てください [太字斜体の強調は私のもの]:

パターンは === トリプルイコールケース包含 2 項中置演算子の左側にあります。これは、メッセージ === が要素ではなくパターンに送信されることを意味します。 === トリプル等しい場合の包含 2 項中置演算子は可換ではありません。

オブジェクトを正規表現で一致させたい場合は、それを正規表現で一致できるもの、つまり文字列として表す必要があります。

class Foo
  def initialize(values)
    @values = values
  end

  def to_str = @values.join
end

[Foo.new(%w[foo bar]), Foo.new(['zoo'])].grep(/oo/)
#=> [#<Foo:0xdeadbeef @values=["foo", "bar"]>, #<Foo:0xdeadbeef @values=["zoo"]>]

ただし、オブジェクトが実際に文字列である場合、つまり、文字列のサブタイプ/特殊化であるとみなせる場合にのみ、これを実行してください。

「3 文字」変換メソッド (to_str、to_int、to_ary、to_float、to_hash など、もちろん、すべてが 3 文字ではないことはわかっています) は、純粋にサブタイプであるオブジェクトに対してのみ実装する必要があります。つまり、to_str は String のサブタイプ (オブジェクト IS-A 文字列) であるオブジェクトによってのみ実装される必要がありますが、技術的な理由により String のサブクラスにすることはできません。

Ruby コア ライブラリでどのオブジェクトが to_str を実装しているかを見ると、String と他の 2 つの 3 つだけがあることがわかります。私なら、他の 2 つは間違っているので実装すべきではないと主張します。 to_int または to_float についても同様です。これらは慎重に使用する必要があります。

これらは暗黙的な変換に使用され、それにはあらゆる危険が伴います。 Foo#to_str を実装する場合は、文字列が予期されるすべての場所で Foo を暗黙的に文字列に変換することを本当に望んでおり、代わりに例外を発生させる必要はありません。

これが言いたいのは、文字列ではない正規表現と何かを一致させようとするのは奇妙であるということです。それがあなたが望むものであると確信していますか、それともあなたのデザインに正しくない何かがあるのでしょうか?