Metaprogramming in Ruby

The Weekly Iteration

This video is only a short sample, but you can access the full version and all our other great content by subscribing.

Video

Notes

A metaprogram is a program that writes other programs, and metaprogramming is the process of writing these metaprograms.

A common use of metaprogramming

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.

Domain-Specific Languages

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

Benefits of DSLs

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.

DSL Structure

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.

Be careful

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.

Further reading

×

15 Full Courses, 100+ Screencasts & New Content Weekly