Wednesday 21 January 2009

Ruby Methods Part 4 - Calling Methods

Ruby has three (at least) ways to call a method on an object. This code illustrates their use. First, a class is defined with four methods, one of which is private, one is a class method and another takes a parameter. The class is instantiated, and then the methods accessed using the various techniques.
# Define a class with three methods
class MethodTest
def public_method
p 'In public_method'
end

def method_with_argument x
p "In public2_method - #{x}"
end

def self.class_method
p 'In class_method'
end

private
def private_method
p 'In private_method'
end
end

# Create instance of class
mt = MethodTest.new

# Invoke methods with the dot operator
mt.public_method
mt.method_with_argument('hello')
MethodTest.class_method
begin
mt.private_method
rescue NoMethodError
p $!
end

# Invoke methods with send
mt.send :public_method
mt.send :method_with_argument, 'Hello'
MethodTest.send :class_method
mt.send :private_method

# Invoke methods as objects
mt.method(:public_method).call
mt.method(:method_with_argument).call 'Hello'
MethodTest.method(:class_method).call
mt.method(:private_method).call

The dot operator
I guess this is the most familiar technique, and is common to other languages, like Java and C++. Private methods are not accessible, and instead a NoMethodError is raised (and a message is produced informing you that the method is private).

The send method
The send method is a part of the Object class and so is available to all objects. It invokes the named method (must be a symbol). As the named method is now being invoked from within the object, this means that all the methods are available, including private and protected methods.

Note that you can also use __send__, in case you have overwritten the send method. Overwriting __send__ will generate a warning that it is a bad idea; Rails uses __send__ a lot, for example, on the assumption that no one would be stupid enough to over-write it.

A good example of using the send method is where you want to access database columns in a large table, where the columns are numbered sequentially, say column0, column1, column2, etc. Rails will handle the generation of methods that will allow @mytable.column1 = 5 and x = @mytable.column1, but how do you get the total of the columns? Let us suppose ten such columns.
total = 0
10.times {|i|
total += send("column" + i.to_s)
}

Strangely, Ruby is quite a stickler for types. In Java or C# you could write "column" + i, but Ruby requires the to_s method to explicitly convert to a string. The send method sends a message (in OO talk) to the named method, column0, column1, etc. To assign a value, you need to append an equals sign to the method name. This loop assigns zero to each column:
10.times {i
send("column" + i.to_s + "=", 0)
}

The send method is a variable length method; just send it the right number of parameters for the method you are invoking.

The method object
Everything in Ruby is an object, including methods. You can access the method object with my_object.method(:my_method). Here is an example of using the method object for the length method of string
s = "string"
puts s.method(:length).class # => Method
puts s.method(:length).call # => 6
puts s.method(:length).methods.sort

# => ["==", "===", "=~", "[]", "__id__", "__send__", ...

As seen earlier, the method that the object represents can be invoked using the call method. As with send, this allows you to access private and protected methods. By the way, you can convert a method to a Proc using the to_proc method.

One important practical difference between using send and using the Method object is that the latter will only work on methods that are actually defined. It will not work for method calls that go though method_missing, including calls to column names for ActiveRecord or calls to the various find methods in Rails. This is because the method has to be defined to become an object.

API for the Method object:
http://www.ruby-doc.org/core/classes/Method.html


Struggling with Ruby: Contents Page

No comments: