Metaprogramming in a nutshell is writing code that writes code. Here’s a really simple example:
<% 100.times do |number| %>
<%= content_tag :strong, "hello" %><br />
<% end %>
Will output:
<strong>hello</strong>
a hundred times.
This really simple example illustrates the power of metaprogramming; in theory, infinite lines of code can be written by just a few lines of your code.
The Rails source code is full of examples of metaprogramming, ever wondered how Active Record is able to provide methods like find_by_username()
?
Here are a couple of examples of how you can clean up your code using metaprogramming techniques.
Some metaprogramming tips
Suppose you have an model, say Comment
, that can exist in several states: “unflagged”, “flagged”, “approved” and “removed”. The comments table has an integer column called “state” that represents each of these states:
class Comment < ActiveRecord::Base
# comment state is 1 by default and then changes as it is flagged, approved etc.
STATES = {
:unflagged => 1,
:flagged => 2,
:approved => 3,
:removed => 4
}
end
You want methods to check if the Comment
is or isn’t in one of these states so you add the following:
class Comment < ActiveRecord::Base
# comment state is 1 by default and then changes as it is flagged, approved etc.
STATES = {
:unflagged => 1,
:flagged => 2,
:approved => 3,
:removed => 4
}
def unflagged?
state == 1
end
def flagged?
state == 2
end
def approved?
state == 3
end
def removed?
state == 4
end
end
Great! Now you can call @comment.flagged?
or @comment.approved?
etc.
This is not ideal though, those four methods are really similar!
Defining methods on the fly.
By using define_method
we can achieve the exact same thing as we have above but with much less fuss! Check this out:
class Comment < ActiveRecord::Base
STATES = {
:unflagged => 1,
:flagged => 2,
:approved => 3,
:removed => 4
}
STATES.each_pair do |key, value|
define_method "#{key}?" do
state == value
end
end
end
For each key in the hash (the names of our states) a new method is created that checks if the object’s state is equal to value. Twelve lines of code condensed into 4 – Magic!
Lets also add methods to change the state of each comment:
class Comment < ActiveRecord::Base
STATES = {
:unflagged => 1,
:flagged => 2,
:approved => 3,
:removed => 4
}.each_pair do |key, value|
define_method "#{key}?" do
state == value
end
define_method "to_#{key}" do
# no point in troubling the database if the state is already == value
update_attribute :state, value unless state == value
end
end
end
When we create a new hash, the hash itself is returned so we can call each_pair
directly on the hash making this code even neater. We now also have methods that will change the state: to_unflagged
, to_flagged
, to_approved
, to_removed
.
If you have models in your applications that exist in one of several states then this technique could really come in handy to clean up your code. You may also be interested in the enum-colum plugin. Enum columns are like string columns but can only have limited amount of permitted values. This is a lot easier to keep track of than using integers like I have in this example.