Friday, 5 December 2008

Ruby Methods Part 2 - Not Overloading!

Method overloading is a common feature of several object-orientated languages. It allows the same name to be used for methods that perform the same, but accept different parameter types. Here is an example in Java:
  // Method overloading in Java
public int myMethod(String name, int age, int shoeSize) {
// do stuff
}

public int myMethod(String name, int shoeSize) {
myMethod(name, 0, shoeSize) {
}

public int myMethod(int age, int shoeSize) {
myMethod("", age, shoeSize) {
}

public int myMethod() {
myMethod("", 0, 0) {
}

// Invoking
public int main(String[] args) {
myMethod("F2Andy", 43, 8);
myMethod("F2Andy", 8);
myMethod(43, 8);
myMethod();
}

Ruby does not support method overloading, something I found very surprising at first. I suspect the reason is the way Ruby handles objects. Java is very strict that what gets sent to a method is exactly what the method expects. That gives (I would imagine) more robustness, but at the loss of flexibility.

Ruby has what could be described as "method over-writing" instead of overloading. In this example, the second test method over-writes the first:
def test s
puts "hello test"
end

def test
puts "goodbye test"
end

test # Causes "goodbye test" to be printed
#test "my string" # Causes an error:
# wrong # of arguments(1 for 0) (ArgumentError)
See here for how to use alias if you still need to access the old methods:
http://www.zweknu.org/blog/index.rhtml?start=77&

Default values and dynamic typing
Ruby has four tricks to mimic method overloading, and I am going to bundle the first two together. The first is default values in the method definition. Simply give a parameter a default value and it becomes optional. The second is the way Ruby handles objects; dynamic typing. Ruby does not care what sort of object you throw at a method, leaving the method writer the chance to handle different classes in different ways. In the next example the method has a default value - "default" - but the method is written in such a way that it will accept any class of object:
def test t = 'default'
puts "goodbye test t=#{t}"
end

test # => "goodbye test t=default"
test 34 # => "goodbye test t=34"

Something to consider, then, is how you handle unexpected objects in your class. One way is to ignore the issue, making the assumption that the method user knows what he is doing and any problems will be caught by the Ruby system.
def test t = 'default'
puts 'goodbye test t=' + t
end

test "one" # => "goodbye test t=one"
#test 2 # => can't convert Fixnum
# into String (TypeError)

Let us look at another example to see what options we have:
def test n
puts n.abs
end

test -4 # => 4
test 2 # => 2
#test "three" # => undefined method `abs'
# for "three": String (NoMethodError)

Rather than relying on your own method calls to generate errors, you might prefer to check that what you receive in your method is what you are expecting. You can then be quite clear about what exceptions your method is throwing, and more importantly, you will not have exceptions thrown half way through you method leaving the system in an unpredictable state.
def test n
raise StandardError.new("Oops, not a Fixnum") unless n.is_a? Fixnum
puts n.abs
end

test -4 # => 4
test 2 # => 2
test "three" # => Oops, not a Fixnum
# (StandardError)

Then again, perhaps you do not care what class the object is. The important question is whether it will behave as you expect. Instead of testing that it is of the right class, you could test that it responds to the appropriate methods calls. In this next example, a new method, abs, is added to the String class (how that is done was the subject of another post). Now the test method checks that the object it is given will respond to the necessary method call. This technique offers a lot more flexibility, but does rely on the API user sending sensible objects to the method. Does it really make sense that a string should return the ASCII code of the first character when asked for its absolute value? However, I would say that that is up to the API user; he has the responsibility now of ensuring the object he sends makes sense. You would need to check for each and every method call in your code.
class String
def abs
self[0]
end
end

def test n
raise StandardError.new('Oops, object does not respond to abs') unless n.respond_to? 'abs'
puts n.abs
end

puts "three".abs # => 116
test -4 # => 4
test 2 # => 2
test "three" # => 116

What would have happened if I had defined the abs method on String to return a string, rather than a Fixnum?
Using hashes for parameters
The third trick is to use a hash, and this is done throughout Rails. In the next example, the method expects a hash. Note that the default is set to be an empty hash, to avoid errors with nil.
def test options = {}
puts "hello #{options[:name]}"
end

test # => "hello "
test "here" # => "hello "
test :name => 'F2Andy' # => "hello F2Andy"

Compared to method overloading, there are advantages on both sides. If you have an existing method and you want to add overloading, it is no problem in Java. In Ruby, chances are your existng method is not expecting a hash and you either have to change your existing method calls or write the new method to expect either a hash or the original type. However, if your Ruby method already accepts a hash, adding more parameters becomes trivial - just add them to method calls if desired, and use in the method code where needed.

Variadic functions
Finally, Ruby supports variadic functions. These are methods that accept a vaiable number of parameters. In Ruby this is indicated by an asterix in the method parameter list, before the last parameter. This will then become an array holding all the "left over" values. Here is an example:
def vari_method name, *values
puts name
values.each { |x|
puts " -#{x}"
}
end

vari_method 'fruit', 'apple', 'banana', 'pear'

When vari_method is invoked, 'fruit' goes into the name variable, and the three other strings are collected into a single array, values. The method prints the name, then lists each member of the array. There is no reason for the members of the array to be of the same class or in any way connected to each other.

Struggling with Ruby: Contents Page

3 comments:

Mark Wilden said...

The reason Ruby doesn't support overloading is simply that there is no way to distinguish a method that expects an int from a method that expects a string.

murphee said...

Take a look at this article:
http://www.jroller.com/murphee/entry/hell_hath_no_fury_like


The static overloading is just a rather odd special case of multi-dispatch (explained in the linked text), or generally: polymorphism.

F2Andy said...

Mark, good point. I was looking for something a little deeper...

murphee, thought-provoking article. My OO background is strongly Java (and a little C#) and that slants my view of Ruby. I keep finding things that are really not Java, and it takes a while to work out what is going on. For the most part, once I get it, it seems better, and when I go back to Java/C# I do miss those features. I am certainly not arguing here that Ruby is poor because it does not support over-loading, though I would hesitate to say it was better bcause it does not. It is just an expression of my surprise.