What is metaprogramming?
After reading the The Rails Tutorial by Michael Hartl, I realized my Ruby on Rails journey wasn’t over. One of the interesting concepts I came across was metaprogramming. This post was heavily inspired by Paolo Perrotta’s wonderful book, Metaprogramming Ruby. In the case of Ruby, metaprogramming enables you to write code that writes other code dynamically. If this is hard to wrap your head around now, hopefully the upcoming example makes it clear.
What is attr_accessor?
You may have come across Ruby code like the following:
class Person
def intialize
@age = 12
end
def age=(n)
@age = n
end
def age
@age
end
end
p = Person.new
p.age = 15 #=>15
attr_accessor gives you the “setter” and “getter” methods without having to define them
So the above Ruby code could be rewritten as:
class Person
attr_accessor :age
def intialize
@age = 12
end
end
p = Person.new
p.age = 15 #=>15
Ah, but how does Ruby do that?
It’s through metaprogramming. Let’s walk through a code example of implementing our own attr_accessor type method. The goal is to implement an attr_accessor type method that doesn’t let you set an age of a person that isn’t an even number.
Example: Implementing your own attr_accessor type method
The code:
module AttrCustom
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_custom(attribute, &validation)
define_method "#{attribute}=" do |value|
raise "Your attribute does not match the block condition" unless validation.call(value)
instance_variable_set("@#{attribute}",value)
end
define_method "#{attribute}" do
instance_variable_get("@#{attribute}")
end
end
end
end #AttrCustom
class Person
include AttrCustom
attr_custom :age do |val|
val%2==0
end
def initialize
@age = 2
end
end
person1 = Person.new
person1.age = 15 #=> … block in attr_custom": Your attribute does not match the block condition (RuntimeError)
person1.age = 20
puts person1.age #=> 20
Include vs. extend
To understand the example code above, you have to understand a bit about the keywords include and extend. In Ruby, when you include a module in a class, you add instance methods to that class. When you extend a module in a class, you add class methods. This is best understood through a sample piece of code.
module Boy
def cooties
puts 'boys have cooties'
end
end
class Girl
include Boy
end
new_girl = Girl.new.cooties #=> boys have cooties
Girl.cooties # NoMethodError: undefined method ‘cooties’ for Girl:Class
class Cousin
extend Boy
end
Cousin.cooties #=> boys have cooties
Cousin.new.cooties #=> NoMethodError: undefined method ‘cooties’ for # (NoMethodError)
You’ll see that by including the Boy module, a new instance of the Girl class will have the instance method cooties. But it will not have a class method for cooties. In contrast, by extending the Boy module, Cousin gets the class method cooties but not an instance method of it (as evidenced by the error above).
Back to attr_custom
Look at the following code again from the AttrCustom module:
def self.included(base)
base.extend(ClassMethods)
end
The "self.included(base)" is telling Ruby that when an AttrCustom module is included in a class, then that class (i.e., base) should get extended with class methods from the ClassMethods module. In our specific case, it allows us to add the attr_custom
method into our Person class when we include the AttrCustom module.
In an upcoming post, I’ll explain about the define_method
call that enables you to define any custom attribute for our Person class.