I'm on Ruby 3.2 and I have a custom class Foo
that (for demonstration purposes) stores an array of values.
I have another code part that is not under my control, that uses Enumerable#grep
and Enumerable#grep_v
to search for string patterns in an array.
I want to make the Foo
class work with Array#grep
. I looked into https://docs.ruby-lang.org/en/3.2/Enumerable.html#method-i-grep and it states that
With no block given, returns an array containing each element for which pattern === element is true:
I tried implementing a #===
method like this
class Foo
def initialize(values)
@values = values
end
def ===(pattern)
@values.any? { |v| v.match?(pattern) }
end
end
But this doesn't seem to work:
[Foo.new(["foo", "bar"]), Foo.new(["zoo"])].grep(/oo/)
=> []
I can also see, when I put a debugger or puts statement inside def ===(pattern)
that it does not seem to be called by the grep command.
Any idea on how this could work?
Look closely at the documentation of Enumerable#grep
[bold italic emphasis mine]:
grep(pattern)
→array
grep(pattern) {|element| ... }
→array
Returns an array of objects based elements of
self
that match the given pattern.With no block given, returns an array containing each element for which
pattern === element
is true […]
The pattern is on the left-hand side of the ===
triple-equals case subsumption binary infix operator, which means the message ===
is sent to the pattern, not to the element. The ===
triple-equals case subsumption binary infix operator is not commutative.
If you want your object to be matchable by a Regexp
, then you need to represent it as something that can be matched by a Regexp
, i.e., a String
:
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"]>]
But, please, only do this if your object actually IS-A string, i.e., if it can be considered to be a subtype / specialization of a string.
The "three letter" conversion methods (to_str
, to_int
, to_ary
, to_float
, to_hash
, etc., and yes, I know they're not all three letters) should only be implemented for objects which genuinely are subtypes. I.e., to_str
should only be implemented by objects which are subtypes of String
(the object IS-A string), but for technical reasons cannot be subclasses of String
.
If you look at which objects implement to_str
in the Ruby core library, you will find that there are only three, String
and two others, and I would argue the two others are wrong and shouldn't implement it. Same goes for to_int
or to_float
. These should be very judiciously used.
They are used for implicit conversions, with all the dangers that entails. If you implement Foo#to_str
, you should be very sure that you really want Foo
to be silently converted to String
everywhere where a String
is expected, and not, say, raise
an Exception
instead.
All this is to say: trying to match something with a Regexp
that is not a String
is weird. Are you sure that is what you want, or is there maybe something in your design that isn't quite right?