This is for an already existing public API that I cannot break, but I do wish to extend.
Currently the method takes a string or a symbol or anything else that makes sense when passed as the first parameter to send
I'd like to add the ability to send a list of strings, symbols, et cetera. I could just use is_a? Array
, but there are other ways of sending lists, and that's not very ruby-ish.
I'll be calling map
on the list, so the first inclination is to use respond_to? :map
. But a string also responds to :map
, so that won't work.
Duck Typing is a type system used in dynamic languages. For example, Python, Perl, Ruby, PHP, Javascript, etc. where the type or the class of an object is less important than the method it defines. Using Duck Typing, we do not check types at all.
Duck Typing is a way of programming in which an object passed into a function or method supports all method signatures and attributes expected of that object at run time. The object's type itself is not important. Rather, the object should support all methods/attributes called on it.
Duck typing is a concept related to dynamic typing, where the type or the class of an object is less important than the methods it defines. When you use duck typing, you do not check types at all. Instead, you check for the presence of a given method or attribute.
Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.
How about treating them all as Arrays
? The behavior you want for String
s is the same as for an Array
containing only that String
:
def foo(obj, arg)
[*arg].each { |method| obj.send(method) }
end
The [*arg]
trick works because the splat operator (*
) turns a single element into itself or an Array
into an inline list of its elements.
Later
This is basically just a syntactically sweetened version or Arnaud's answer, though there are subtle differences if you pass an Array
containing other Array
s.
Later still
There's an additional difference having to do with foo
's return value. If you call foo(bar, :baz)
, you might be surprised to get [baz]
back. To solve this, you can add a Kestrel:
def foo(obj, arg)
returning(arg) do |args|
[*args].each { |method| obj.send(method) }
end
end
which will always return arg
as passed. Or you could do returning(obj)
so you could chain calls to foo
. It's up to you what sort of return-value behavior you want.
A critical detail that was overlooked in all of the answers: strings do not respond to :map
, so the simplest answer is in the original question: just use respond_to? :map
.
Since Array
and String
are both Enumerables
, there's not an elegant way to say "a thing that's an Enumberable, but not a String," at least not in the way being discussed.
What I would do is duck-type for Enumerable (responds_to? :[]
) and then use a case
statement, like so:
def foo(obj, arg)
if arg.respond_to?(:[])
case arg
when String then obj.send(arg)
else arg.each { |method_name| obj.send(method_name) }
end
end
end
or even cleaner:
def foo(obj, arg)
case arg
when String then obj.send(arg)
when Enumerable then arg.each { |method| obj.send(method) }
else nil
end
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With