Reflection
Reflection is the process of discovering information about objects at run time. You can discover the methods available for a class by calling the
methods method. The base class for everything in Ruby is Object, so here's a look at some of the methods available to every object in Ruby:
irb(main):001:0> o = Object.new
=> #<Object:0x3f8feb4>
irb(main):002:0> o.methods
=> ["inspect", "taguri", "clone", "public_methods"
, "taguri=", "display", "instance_variable_defined
?", "equal?", "freeze", "methods", "respond_to?",
...many more methods listed...
The result of calling
methods is an array of strings containing each method name available on that object. You can think of a class much like a Hash; calling
methods is like getting the keys to that hash table.
Knowing the methods wouldn't be interesting unless you could do something with them, of course. To call the method, you call the
send method. The following operations are equivalent:
irb(main):003:0> o.inspect
=> "#<Object:0x3f8feb4>"
irb(main):004:0> o.send "inspect"
=> "#<Object:0x3f8feb4>"
Ruby gives you an opportunity to handle unknown methods by defining the
method_missing method on your class:
irb(main):139:0> class Object
irb(main):140:1> def method_missing(*args)
irb(main):142:2> puts args
irb(main):143:2> end
irb(main):144:1> end
=> nil
irb(main):145:0> o.foobar 1, 2, 3
foobar
1
2
3
=> nil
As you can see, the arguments passed to
method_missing include the requested method as well as all the arguments passed to that method. A better definition would probably be:
def method_missing(method, *args)
Meta-Programming
Although Ruby does not have properties, you can use the fact that method invocation does not (usually) require parenthesis to simulate properties. You also need to leverage the fact that Ruby treats methods that end with
= specially, making them act like setters.
You might define a person class as such:
irb(main):001:0> class Person
irb(main):002:1> def age
irb(main):003:2> @age
irb(main):004:2> end
irb(main):005:1> def age=(value)
irb(main):006:2> @age = value
irb(main):007:2> end
irb(main):008:1> end
=> nil
Now you can use instances of Person, and treat age like a property of the person:
irb(main):009:0> p = Person.new
=> #<Person:0x89c5678>
irb(main):010:0> p.age
=> nil
irb(main):011:0> p.age = 42
=> 42
irb(main):012:0> p.age
=> 42
If you wanted age to default to something other than
nil, you could set it in an
initialize method.
This code feels very boilerplate. If this were a language like C#, you'd probably be tempted to use things like snippets in Visual Studio or even static code generation to automatically generate the reader and writer of a property.
With Ruby, though, you can use meta-programming to create these things with just a small amount of effort. Ideally, you'd like to be able to write something like:
class Person
prop :age
end
You'll define a class (static) method on Object so that it's available to use when you're defining your class. You'll also use a method you haven't seen yet, the
class_eval method:
irb(main):001:0> class Object
irb(main):002:1> def self.prop *names
irb(main):003:2> names.each { |name|
irb(main):004:3* self.class_eval "
irb(main):005:3" def #{name}
irb(main):006:3" @#{name}
irb(main):007:3" end"
irb(main):008:3> self.class_eval "
irb(main):009:3" def #{name}=(value)
irb(main):010:3" @#{name} = value
irb(main):011:3" end"
irb(main):012:3> }
irb(main):013:2> nil
irb(main):014:2> end
irb(main):015:1> end
=> nil
The
class_eval method as used above is what ends up creating the methods. It evaluates the string just as if you'd done that in the context of the class itself. So you can write your methods just as if you'd been writing them inside the class all along.
Each name passed to the
prop method adds two methods to the new class: a getter and a setter. The end result string replaces
#{name} with the name that you passed to the
prop.
Now you can use
prop in your class definition:
irb(main):016:0> class Person
irb(main):017:1> prop :age, :name
irb(main):018:1>
irb(main):019:1* def initialize(age, name)
irb(main):020:2> @age = age
irb(main):021:2> @name = name
irb(main):022:2> end
irb(main):023:1> end
=> nil
irb(main):024:0> p = Person.new(36, "Brad")
=> #<Person:0x89c0768 @age=36, @name="Brad">
irb(main):025:0> p.age
=> 36
irb(main):026:0> p.name
=> "Brad"
With these kinds of facilities at your disposal, you can quickly create classes at a much higher level, using these kinds of meta-programming tricks to do much of the work for you, without needing to rely on editor snippets or compiler-time code generation.
Next Steps
This article really just scratched the surface of the facilities available in Ruby. Learning Ruby today will help you prepare for when Ruby is available on .NET and in Silverlight. Having such a powerful dynamic language will be a welcome addition to your programmer's tool belt, but more importantly, it will help you start to think about problems and solutions in a new way.
To continue your Ruby education, I strongly recommend
Programming Ruby: The Pragmatic Programmer's Guide by David Thomas and Andrew Hunt (Addison-Wesley Professional), sometimes called "The Pickaxe" for its cover picture.