tags:

views:

252

answers:

7

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.

A: 

Can you just switch behavior based on parameter.class.name? It's ugly, but if I understand correctly, you have a single method that you'll be passing multiple types to - you'll have to differentiate somehow.

Alternatively, just add a method that handles an array type parameter. It's slightly different behavior so an extra method might make sense.

Jamie Hale
Your second comment is a better solution, but I'm hoping for more from the stackoverflow gurus.The first one has the same problem as `is_a? Array`: a user can send a string that's not a `String` or a list that's not an `Array`.
Bryan Larsen
I'm not sure how common non-`String` strings are, though. non-`Array` lists are quite common -- many objects are `Enumerable`. Unfortunately, so are strings....
Bryan Larsen
A: 

Use Marshal to serialize your objects before sending these.

Patrick Daryll Glandien
I'm confused. `send` is the method that calls a function by name in Ruby. How would Marshal help?
Bryan Larsen
+1  A: 

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
Clinton R. Nixon
Thanks for the extended explanation, but in the original question I asked for something cleaner than `is_a? Array`. Using `is_a? String` (or a variant like you've shown) breaks the original API for anybody who is using something that quacks like a String for `send`. In fact, most users probably use `Symbol` instead. I could use `x.is_a?(String) || x.is_a?(Symbol)`, but that's not very ruby-ish, and may break the API for some of my users.
Bryan Larsen
A: 

If you don't want to monkeypatch, just massage the list to an appropriate string before the send. If you don't mind monkeypatching or inheriting, but want to keep the same method signature:

class ToBePatched
    alias_method :__old_takes_a_string, :takes_a_string

    #since the old method wanted only a string, check for a string and call the old method
    # otherwise do your business with the map on things that respond to a map.
    def takes_a_string( string_or_mappable )
        return __old_takes_a_string( string_or_mappable ) if String === string_or_mappable
        raise ArgumentError unless string_or_mappable.responds_to?( :map )
        # do whatever you wish to do
    end
end
A: 

Perhaps the question wasn't clear enough, but a night's sleep showed me two clean ways to answer this question.

1: to_sym is available on String and Symbol and should be available on anything that quacks like a string.

if arg.respond_to? :to_sym
    obj.send(arg, ...)
else
    # do array stuff
end

2: send throws TypeError when passed an array.

begin
  obj.send(arg, ...)
rescue TypeError
  # do array stuff
end

I particularly like #2. I severely doubt any of the users of the old API are expecting TypeError to be raised by this method...

Bryan Larsen
The first solution makes sense, since send actually does expect a symbol. But I hope you're not serious about the second solution. Exceptions are not control flow.
Sarah Mei
@Sarah: random, but I think roughly 40% of all your comments and responses on SO include the sentence "Exceptions are not control flow."
Telemachus
@Telemachus - at least recently. :)
Sarah Mei
+1  A: 

Let's say your function is named func

I would make an array from the parameters with

def func(param)
  a = Array.new
  a << param
  a.flatten!
  func_array(a)
end

You end up with implementing your function func_array for arrays only

with func("hello world") you'll get a.flatten! => [ "hello world" ] with func(["hello", "world"] ) you'll get a.flatten! => [ "hello", "world" ]

Arnaud
+2  A: 

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.

James A. Rosen
Much more beautiful than my original post.I can never get used to the * operator.
Arnaud