Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant way of duck-typing strings, symbols and arrays?

Tags:

ruby

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.

like image 301
Bryan Larsen Avatar asked Aug 25 '09 19:08

Bryan Larsen


People also ask

What is duck typing example?

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.

What is duck typing design?

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.

What is a duck typed language?

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.

Is duck typing structural?

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.


3 Answers

How about treating them all as Arrays? The behavior you want for Strings 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 Arrays.

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.

like image 83
James A. Rosen Avatar answered Oct 23 '22 06:10

James A. Rosen


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.

like image 22
Jason Sims Avatar answered Oct 23 '22 06:10

Jason Sims


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
like image 27
Clinton Dreisbach Avatar answered Oct 23 '22 04:10

Clinton Dreisbach