Want to see the full-length video right now for free?
A metaprogram is a program that writes other programs, and metaprogramming is the process of writing these metaprograms.
Here's an example of metaprogramming in Ruby:
class Post
def initialize(status)
@status = status
end
%w(published unpublished draft).each do |possible_status|
define_method("#{possible_status}?") do
@status == possible_status
end
end
end
This seems like it saves time, because we don't need to write separate methods
for published?
, unpublished?
, and draft?
. However, there are tradeoffs.
For example, metaprogramming like this makes searching for method definitions
later difficult. It's certainly faster to type, but it's harder to find and read
later. Since we spend so much more time reading code than writing it, code
that's easier to write than read is actually a bad tradeoff.
A Domain-Specific Language, or DSL, is a custom language that solves a specific domain or problem. In Ruby's case, a DSL is still written in Ruby, but probably looks pretty different from standard Ruby code.
Some common Ruby examples are Rails routes and Factory Girl. Factory Girl has complicated internal code, but it allows you to write expressive, declarative code:
FactoryGirl.define do
sequence :github_username do |n|
"github_#{n}"
end
factory :user do
description "Learn all about Git"
github_username
trait :admin do
admin true
end
end
end
Well-written DSLs cover implementation details that don't matter (like associating factory-built objects with each other) while letting you focus on the parts that change. Poorly-written DSLs are a bad abstraction. They will make programmers' lives harder because the internals are hidden and it doesn't expose enough of the underlying complexity, or it exposes complexity that isn't useful. It's a hard line to walk, but a good DSL can save a tremendous amount of time.
One possible problem with DSLs is that, since it's not a general-purpose language, it might not be possible to change how it works for your specific use case. It's an area where the Factory Girl maintainer has spent quite a bit of time.
Rails routes, RSpec, and Factory Girl all use do
blocks:
describe "User" do
# ...
end
FactoryGirl.define do
# ...
end
Rails.application.routes.draw do
# ...
end
Ruby's blocks let us delay evaluation until the block is run (at a later point,
by the DSL). The blocks also let us run code in the context of another class,
using instance_eval
. For more on exactly how that works, check out Writing a
Domain-Specific Language in
Ruby.
That block of code is sent to another class inside the DSL code, and it gets run in the DSL's context. That context change makes it easy for the DSL author to hide complexity and only expose what's necessary.
If there is a less-complicated solution to a problem, reach for that first. Metaprogramming is usually not a good first solution to a problem, and DSLs require a good understanding of the problem's domain. Once you do understand the problem well, though, DSLs are a great option.