Ever tried to explain DSLs (domain specific languages) to someone, so that they know how wonderful Ruby is, and just run out of useful examples? Try this piece of Street Magic on them…
The effect
See if your unsuspecting friend can figure out what this code will do. If they can, then say, “That’s because you are a domain expert in English. Good for you. Now use Ruby you Java boy.”
>> I.say "I love the Ruby language"
You said, 'I love the Ruby language'
Cool, yes? Yes.
How this trick works
To set this up for your unsuspecting Ruby noob, type the following into your console/irb:
>> class I; def self.say(text); puts "You said, '#{text}'"; end; end
=> nil
Now you have a constant I upon which you can call class methods.
In long hand, this is:
class I
def self.say(text)
puts "You said, '#{text}'"
end
end
Create other pronoun classes, such as You, Everyone, etc and give them methods representing verbs, such as say, said, want_to.
For example,
class You
def self.cannot(action, target)
puts "Didn't your mother tell you never say never? Of course you can #{action} #{target}"
end
end
Which delights us with:
>> You.cannot :learn, "Ruby"
Didn't your mother tell you never say never? Of course you can learn Ruby
Do spend two hours on a hack to save yourself one hour of time?
Is being a great coder important to you, even if you don’t know why its important?
Do you derive a sense of pride from a few lines of code you wrote?
I thought of all these questions whilst watching a great guitar clip… he’s gone beyond learning Pachelbel’s Canon in D Major, and overlayed his own code. Underneath the hat, I’m sure he’s smiling.
His pride for his guitar work makes me happy for my coding. You?
You go on a dragon-hunting, adventure game using sexy Ruby syntax (a domain-specific language/DSL for adventure games?). Here is some sample syntax for defining a monster class:
class Dragon < Creature
life 1340 # tough scales
strength 451 # bristling veins
charisma 1020 # toothy smile
weapon 939 # fire breath
end
The life, strength, charisma and weapon class methods are generated by a traits class method called against the Creature class (read the chapter).
class Creature
traits :life, :strength, :charisma, :weapon
end
Read this chapter section many times and admire the beauty of the idea (and amuse yourself with his writing style!).
But there is one small improvement that could be made: currently, after setting the trait methods (e.g. life 1340 sets the life trait to 1340), you cannot access the class's trait values directly via their original trait method. That is, you cannot call Dragon.life to retrieve the value 1340.
This is due to a limitation of the define_method method being used. The relevant code from _why's book is:
def self.traits( *arr )
# 2. Add a new class method to for each trait.
arr.each do |a|
metaclass.instance_eval do
define_method( a ) do |val|
@traits ||= {}
@traits[a] = val
end
end
end
The method creator define_method uses a block to define the generated method body. The parameters for the block (val in the example above) become the arguments of the method once its been added to the class. That is, if we call traits :life on our Creature class, then a class method will be generated that requires one argument - the value of the trait. That is, it will generate the following method:
class Creature
def life(val)
@traits ||= {}
@traits[:life] = val
end
end
Now, back to the problem. How do we support the syntax Dragon.life? To achieve this, the generated method would need to look like:
class Creature
def life(val = nil)
@traits ||= {}
return @traits[:life] if not val
@traits[:life] = val
end
end
That is, we need a default value for our method argument. But... blocks don't allow parameters to have default values. We cannot do the following:
define_method( a ) do |val = nil|
@traits ||= {}
return @traits[a] if not val
@traits[a] = val
end
A pity, yes.
So, we need to generate our methods differently. The solution is as follows:
metaclass.class_eval <<-EOS
def #{a}(val=nil)
@traits ||= {}
return @traits[:#{a}] if not val
@traits[:#{a}] = val
end
EOS
The eval methods allow a string to be passed to them. So, we shall pass it a string that defines a new method the old fashioned way: using the def method constructor. Thus it allows us to have default values for our arguments.
Use the splat! (*) to allow zero or more arguments. Pluck the first one off to represent the incoming argument or nil.
def self.traits( *arr )
arr.each do |a|
metaclass.instance_eval do
define_method( a ) do |*val|
val = val.first
@traits ||= {}
return @traits[a] if not val
@traits[a] = val
end
end
end
end
There are unanswered questions about its support for 2+ arguments (where we only need support for 0 or 1) that shall remain unanswered for the sake of this simple hack. But feel free to start throwing exceptions around if your users need protection from themselves.
Rails’ active_support library adds some wonderful functions into standard Ruby classes. Some we all use day-in-day out are attr_accessor and its class-level equivalent, cattr_accessor.
But cattr_accessor doesn’t work the way you (read, “me”) thought at first glance when you use subclasses. I thought if I declared a class accessor in the superclass, then I would have independent class attributes for all my subclasses. Apparently not…
>> class Parent; cattr_accessor :val; end
=> [:val]
>> class Child1 < Parent; end
=> nil
>> class Child2 < Parent; end
=> nil
>> Child1.val = 4
=> 4
>> Child2.val
=> 4
>> Child2.val = 5
=> 5
>> Child1.val
=> 5
Child1.val and Child2.val seem to be the same value. Not very independent at all. Internally, the classes share a common class attribute. This is useful in certain circumstances, but not what I was looking for.
Instead, I found class_inheritable_accessor.
>> class Parent; class_inheritable_accessor :val; end
=> [:val]
>> class Child1 < Parent; end
=> nil
>> class Child2 < Parent; end
=> nil
>> Child1.val = 4
=> 4
>> Child2.val = 4
=> 4
>> Child2.val = 5
=> 5
>> Child1.val
=> 4
Lovely. Each subclass will have an independent value once you’ve assigned it a value explicitly, else it will pick up the value from its superclass.
UPDATE: class_inheritable_accessor and co. actually clone the superclass’s inherited attributes, rather than just referencing them.