Metaprogramming and Domain-Specific Languages

This chapter briefly investigates two advanced concepts which Ruby provides: metaprogramming and building domain-specific languages which are also valid Ruby code.


Ruby is known to have very powerful metaprogramming capabilities, that is, defining language structures (classes, modules, methods) at runtime. Unlike many other languages, Ruby’s metaprogramming does not use special constructs different from “normal” programming, like macros, decorators or templates. The power of Ruby’s metaprogramming comes from two facts:

  • Everything is an object;
  • Core classes are hackable.

Everything is an object

That also includes core language concepts like classes and methods. They can be inspected and changed at runtime.

class A
  def m(x, y)

A.class   #=> Class
A.methods #=> [:new, :allocate, :superclass, ....
A.instance_method(:m) #=> #<UnboundMethod: A#m>
A.instance_method(:m).parameters #=> [[:req, :x], [:req, :y]]

Core classes are hackable

Core classes (like Class, Module, Method) provide methods to change their behavior and define new concepts with regular Ruby code:

# Define methods with names stored in variable
class A
  [:+, :-, :*, :/].each do |op|
    define_method(op) {
      # ...

# Call methods, names stored in variable
[:+, :-, :*, :/].map { |op| 100.send(op, 10) } #=> [110, 90, 1000, 10]

# Create classes and assign to constants
[:A, :B, :C].each { |name| Kernel.const_set(name, }

Metaprogramming example

It is a common pattern to have some variable calculated on first method call:

def value
  @value ||= expensive_calculation

With metaprogramming, we can encapsulate this pattern into memoize method, allowing us to write code like this:

memoize def value

The implementation can look like this:

def self.memoize(method_name) # define class-level method receiving method name to memoize
  original = instance_method(name) # storing previous implementation in variable, it is UnboundMethod
  ivar_name = :"@#{name}"

  define_method(name) do  # redefine method with name provided
    # return variable if it is set
    return instance_variable_get(ivar_name) if instance_variables.include?(ivar_name)
    # or calculate variable with previous method implementation and store it
    instance_variable_set(ivar_name, original.bind(self).call)

Note: This implementation is naive and just shows the principle. See third-party libraries like memoist for a proper implementation.

See Language Core classes documentation to understand what you can do with core objects.

Domain-Specific Languages

Ruby is naturally flexible enough for defining clean and readable sublanguages by means of methods and blocks. Short example (from the syntax of the RSpec testing library):

RSpec.describe Calculator do
  subject { Calculator.add(number1, number2) }
  let(:number1) { 5 }
  let(:number2) { 6 }

  it 'adds numbers' do
    expect(subject).to eq 11

All components in this example are built with just methods, their arguments and blocks:

RSpec.describe Calculator do                          # Method RSpec.describe() with argument Calculator and
                                                      # block of code (which will be later evaluated)
  subject { Calculator.add(number1, number2) }        # Method subject() with block of code defining test subject
  let(:number1) { 5 }                                 # Method let() with argument :number1 and block of code
  let(:number2) { 6 }

  it 'adds numbers' do                                # Method it() with argument 'adds numbers' and block of code
                                                      # defining what to test
    expect(subject).to eq 11

Note how varying elements of syntax (optional paretheses around method arguments and {} vs do / end around blocks) allows the creation of boilerplate-less tests.