views:

349

answers:

3

Hopefully I haven't misunderstood the meaning of "duck typing", but from what I've read, it means that I should write code based on how an object responds to methods rather than what type/class it is.

Here's the code:

def convert_hash(hash)
  if hash.keys.all? { |k| k.is_a?(Integer) }
    return hash
  elsif hash.keys.all? { |k| k.is_a?(Property) }
    new_hash = {}
    hash.each_pair {|k,v| new_hash[k.id] = v}
    return new_hash
  else
    raise "Custom attribute keys should be ID's or Property objects"
  end
end

What I want is to make sure that I end up with a hash where the keys are an integer representing the ID of an ActiveRecord object. I don't particularly enjoy having to iterate through the hash keys twice with all? to determine if I need to grab the ID's out.

Of course, I'll accept any other suggestions to improve this code as well :)

+9  A: 

How you write this method should depend on whether you expect an exception to be thrown during the course of normal program execution. If you want a readable exception message because an end-user might see it, then throwing one manually makes sense. Otherwise, I'd just do something like this:

def convert(hash)
    new_hash = {}
    hash.each_pair { |k,v| new_hash[ k.is_a?(Integer) ? k : k.id ] = v }
    return new_hash
end

This will accomplish exactly the same thing, and you'll still get an exception if an array key doesn't have an id field. Even better, this uses a little more duck typing because now anything that has an id field will be acceptable, which is better than explicitly checking for something being a Property. This makes your code more flexible, especially when unit testing.

We still have an explicit check for integer objects, but this kind of occasional special case is usually acceptable, especially when checking for built-in data types.

Eli Courtwright
Great, descriptive answer with some very Rubyish code, Eli. Thanks very much for the reply.
Brent Dillingham
The problem with that is that EVERY object in Ruby has an #id method. It's defined on Object, and gives a unique reference in the Ruby interpreter to that Object. It is deprecated though, so while you will get a warning, you won't get an exception.
madlep
+2  A: 

Duck typing is really just a nuanced version of polymorphism. In a statically typed language like Java you'd have to create an explicit interface that told the compiler all of the methods that a particular variable can accept. With a dynamic language like Ruby the interfaces still exist in an abstract sense, they're just implicit.

The problem is the fact that you're accepting two different data structures into one method. The way to make duck typing work is to require that all the objects that get passed to your method obey the same contract (i.e. it's always a hash of Integers to [Foo] objects.) The process of converting a hash with Property keys into the correct structure should be the job of the client code. That can be done very easily with a simple wrapper class or a conversion function consisting of just the body of your elseif clause.

Bottom line it's up to the guy calling the method to make sure his parameters all quack the way your method expects them to quack. If they don't, he's the one who need's to figure out how to make his turkey quack like a duck, not you.

Mike Deck
A: 

What I want is to make sure that I end up with a hash where the keys are an integer representing the ID of an ActiveRecord object.

You should probably check for that when you're creating/inserting into the hash. You could try something like this:

h = {}
def h.put obj
  self[obj.id]=obj
end

or maybe

h = {}
def h.[]= key, value
  raise "hell" unless key == value.id
  super
end