110 liens privés
(this snippet was tested with ruby 2.3)
RUBY helps a lot to write DRY code.
This feature is both great and very disturbing.
ruby on Rails, for example, is full of meta-programming.
That's how the rails "magic" happens.
Let's point out it's not magicall, and pretty simple to explain.
NB : This snippet requires some basic understanding of Object Oriented Programming.
I tried to take baby steps to explain it.
It should be very easy to understand.
Meta-programming, what is it ?
Let's assume we play with different living-dead species : Zombies and Vampires, Werevamps. Vampires have appetite for blood, whereas Zombies have appetite for brains, and Werevamps crave for blood and/or flesh.
We would like to be able to define a "macro" that let's us define what a class has appetite for, so we could write :
class Zombie < LivingDead
has_appetite_for :brains
end
class Vampire < LivingDead
has_appetite_for :blood
end
class Werevamp < LivingDead
has_appetite_for :blood
has_appetite_for :flesh
end
bob = Werevamp.new
bob.regime
#=> [:blood, :flesh]
bob.hunt_blood
#=> "bob is hunting for blood"
bob.hunt_flesh
#=> "bob is hunting for flesh"
The "regime, " methods exists just because Werevamp is derived from LivingDead.
But how do we make that happening ?
The answer is "Meta-programming", also known as "macro".
This is precisely what rails classes are doing beind the hood. It's no magic.
For instance, your has_many :persons
in your Company
model gives you methods like Company.first.persons
wich returns a collection of Person objects. Let's see how this is done, step by step
Monkey patching an object
We know how to monkey patch an object :
fred = "Zombie"
#=> "Zombie"
fred.class
#=> String
def fred.has_appetite_for
"brains"
end
#=> :has_appetite_for
fred.has_appetite_for
#=> "brains"
We monkey patched the fred
object.
It now has a new method called :has_appetite_for
fred is an object. It's a simple String.
Since a class is an object of class Class, we could do the same on the class itself.
Patching a class
class Zombie
end
#=> nil
Zombie.class
#=> Class
my_zombie_classs = Zombie
#=> Zombie
my_zombie_class.class
#=> Class
def my_zombie_class.has_appetite_for
"brain"
end
#=> :has_appetite_for
Zombie.has_appetite_for
#=> "brain"
Pushing it into the class
We could put that the class itself.
Let's do that with the Vampire class
class Vampire
def self.has_appetite_for
"blood"
end
self.has_appetite_for
end
#=> "blood"
Vampire.has_appetite_for
#=> "blood"
Within the class definition, self refers to the Vampire class itself. Therefore, we could replace self
with Vampire
in the above example, it would do the same thing.
Now we would like to have a method called "has_appetite_for" that gets the food as an argument. Let's assume we have a new kind of living-dead called "Werevamp". A Werevamp has taste for either blood or flesh.
class Werevamp
def self.has_appetite_for(food)
puts "#{self} has appetite for #{food}"
end
has_appetite_for :flesh
has_appetite_for :blood
end
#=> "Werevamp has appetite for flesh"
#=> "Werevamp has appetite for blood"
bob = Werevamp.new
#=> "Werevamp has appetite for flesh"
#=> "Werevamp has appetite for blood"
If we remove the "self" call, ruby assumes that has_appetite_for
is called on self
One last step : define a method within our method : to do that we use
define_method
class Werevamp
def self.has_appetite_for(name)
puts "#{self} has appetite for #{name}"
define_method("hunts_#{name}") do
puts "A #{self.class} is now hunting for #{name}"
end
end
has_appetite_for :flesh
has_appetite_for :blood
end
#=> "Werevamp has appetite for flesh"
#=> "Werevamp has appetite for blood"
#=> => :hunts_blood
bob = Werevamp.new
#=> => #<Werevamp:0x00000001e244c8>
bob.hunts_blood
#=> A Werevamp is now hunting for blood
#=> =>nil
# reminder : puts returns nil
Pushing it on a parent class
Now we have everything we need to push the behavior a parent class.
class LivingDead
attr :name, :food # this too is meta-programming, by the way.
def initialize(name)
@name = name
end
def self.has_appetite_for(food_name)
puts "#{name} has appetite for #{food_name}"
define_method("hunts_#{food_name}") do
puts "#{self.name} is hunding for #{food_name}"
end
end
end
class Zombie < LivingDead
has_appetite_for :brain
end
john = Zombie.new("J")
#=> => #<Zombie:0x00000002ad8610 @name="J">
john.hunts_brain
#=> J is hunting for brain
#=> => [:brain]
Important note :
One does NEED to understand the meaning of self here.
When ruby executes code, the self variable is contextual.
Wich means that in the class definition, self refers to the class itself.
In a class definition, self.class is a Class.
While when the code is being executed on the instance, self refers to the instance.
Let's push that to a module
Now we can create a LivingDead module.
Let's add a feature : we want to keep track of who eats what.
module LivingDead
attr :regime
@regime = a = Hash.new{ |hash, key| hash[key] = [] }
def self.add_to_regime name, sender
@regime["#{sender}"] << name
end
def self.get_regime sender
@regime["#{sender}"]
end
private
def self.regimes
@regimes
end
public
class Base
include LivingDead
attr :name; :regime
def initialize(name)
@name = name
end
def self.has_appetite_for(food_name)
puts "#{name} has appetite for #{food_name}"
# send :add_to_regime, food_name
LivingDead::add_to_regime food_name, self
define_method("hunts_#{food_name}") do
puts "#{name} is now hunting for #{food_name}"
end
define_method (:get_regime) do
LivingDead::get_regime "#{self.class}"
end
end
def regimes
LivingDead::regime
end
end
end
class Zombie < LivingDead::Base
has_appetite_for :brain
end
class Vampire < LivingDead::Base
has_appetite_for :blood
end
class Werevamp < LivingDead::Base
has_appetite_for :flesh
has_appetite_for :blood
end
john = Zombie.new("John")
bob = Vampire.new("Bob")
jenny = Werevamp.new("Jenny")
john.get_regime
#=> => [:brain]
bob.get_regime
#=> => [:blood]
jenny.get_regime
#=> => [:flesh, :blood]
john.hunts_brain
bob.hunts_blood
jenny.hunts_flesh
include LivingDead
LivingDead::regime
#=> => {"Zombie"=>[:brain], "Vampire"=>[:blood], "Werevamp"=>[:flesh, :blood]}
jenny.regimes
#=> => {"Zombie"=>[:brain], "Vampire"=>[:blood], "Werevamp"=>[:flesh, :blood]}
LivingDead::regimes
#=> => nil because private !
Important note :
See how the "self" is behaving. When it's interpreted as a code from the class, it's the class. When it's interpreted from within the object, it's the object name.I included a private sample so we can also see here that it does work as expected.
A good example of the above note :
define_method (:get_regime) do
LivingDead::get_regime "#{self.class}"
end
We have to consider here that the "self" will be interpreted WITHIN the object.
Therefore, the self.class is now being used.