Tuesday, February 21, 2006
The Way of Meta - Part III
The Way of Meta
~
V. 0.0.3
Writing Method Generators
I am going to write a simple example that illustrates how you can create a method generator: a construct used within a class definition as a shortcut to generate richer method semantics, in the same way that attr_accessor generates enrich class behaviour by adding new methods.
This example concerns the development of a 'synonym' generator. A synonym is similar to an alias because it provides different calls to achieve the same result. It is different from an alias in that an alias generates a copy of a method, rather than a different name to call the same method.
If an aliased method gets overridden, its aliases will keep calling the old code. Conversely, a synonym of a method is a simple light code layer that keeps calling a certain method name. If the aliased method gets overwritten, then all its synonyms will start calling the new version of the method.
Let's start from our intent; let's visualise how we would like to use synonyms.
class Person
def say
"hi"
end
def walk
"one step"
end
synonym :say => [ :talk , :discuss , :greet ],
:walk => [ :stride, :run, :jump ]
end
Here we are declaring that both the method 'say' and the method 'walk' have several synonyms.
We also expect the following statements to be true
p1 = Person.new
p p1.talk == "hi"
p p1.discuss == "hi"
p p1.greet == "hi"
p p1.stride == "one step"
p p1.run == "one step"
p p1.jump == "one step"
They should become false when we override the old methods:
class Person
def say
"yo"
end
end
p p1.say == "yo"
p p1.talk == "yo"
p p1.discuss == "yo"
p p1.greet == "yo"
Enough talking, we have all the intent we need for a first stab at the problem. Let's dive straight into the code.
First of all we need a convenient way to process the arguments passed to the 'synonym' generator.
Consider how we will need to elaborate the following hash:
:say => [ :talk, :discuss, :greet ],
:walk => [ :stride , :run , :jump ]
This is just a convenient idiom that we use to express the following relationships:
[
[ :say, :talk ],
[ :say, :discuss],
[ :say, :greet ],
[ :walk, :stride ],
[ :walk, :run ],
[ :walk, :jump ]
]
Let's write some code that can take us from the first compact format to the second explicit representation. We will call the relationships 'associations' and we will provide a method that allows us to iterate over a hash one association at a time.
# expands a => [b1,b2,b3] associations to [a,b1], [a,b2], [a,b3], etc..
class Hash
def each_association &block
self .each_pair do | assoc_key, assoc_targets|
assoc_targets.to_a.each do |assoc_target|
block.call assoc_key, assoc_target
end
end
end
end
Now that we have a way to visit our associations, we will generate a new synonym method for each association:
class Module
# associations: ( old_name => [new names] )*
def synonym associations
associations.each_association do
| original_name_sym, new_name_sym|
define_method( new_name_sym){ |* args|
self .send original_name_sym, *args
}
end
end
end
For each association specified in the synonym generator we inject a new method in our class. The new method has the name specified on the right hand side of the association, and all it does is to invoke the original method of the class.
The synonym generator has been defined within the Module class, so that it becomes automatically available within the context of the classes 'Module' and 'Class'.
We can now use the generator even to expand existing classes with new developer friendly names.
Here we tweak the Array class:
Array.class_eval do
synonym :size => [ :count, :n_elements ]
end
p [1, 2,3 ].count == 3
p [1, 2,3 ].n_elements == 3
And here we provide a way to talk to a Person, rather than giving him orders!
class Person
def calculate( a,op,b)
a.send op, b
end
end
p (p1.calculate 3, :+, 2) == 5
class Person
synonym :calculate => :how_much ?
end
p (p1.how_much? 3, :+, 2) == 5
It is worth pointing out a small syntactic trick that we used to handle both cases of single ( :calculate => :how_much ?) and multiple synonyms (:size => [ :count , :n_elements ]).
We get the right hand side of the hash element and we apply 'to_a' to it, turning it into an array.
…
assoc_targets.to_a.each do |assoc_target|
…
end
…
If the element is not an array, it gets turned into one. If it is already an array, it is left unchanged. Whatever the case, we are left with a simple array to operate on in the end.
Also, you could use the 'alias' method directly, instead of having to define a new method. Replace the 'define_method' call with module_eval "alias #{new_name_sym} #{original_name_sym}"
Ruby rocks!
For what concerns method_missing I have used it a lot in the past, but I think it tends to make errors harder to find and I always try to generate a method rather than doing dynamic dispatching via method-missing.
However there are a few interesting method-missing tricks I'll put in a future post.
<< Home