I recently finished the book Metaprogramming Ruby by Paolo Perrotta and found it very informative. Paolo introduces several metaprogramming techniques which he referes to as "spells". I've used many of the techniques before, but was never really aware of their "formal" names.

The idioms defined in the book are so helpful that I created a reference based on them for my own use. Hopefully it helps others out too.


Open Classes

In Ruby all classes are open, meaning that you can define new functionality for the class after the class has already been defined.

# define original class
class Example
  def say_hello
    puts "hello"
  end
end

# re-open the class
class Example
  # add new functionality
  def do_stuff
    puts "doing stuff"
  end
end

# usage
Example.new.say_hello # => hello
Example.new.do_stuff # => doing stuff

# other ways to reopen the class

# open the instance of the class definition
Example.instance_eval do
end

# open the eigenclass
# you need to understand Ruby's object model
# to fully understand what the eigenclass is
class << Example
end

# syntactic sugar for class << Example
Example.class_eval do
end

Monkeypatch

Monkeypatching is a somewhat negative term that refers to the ability to re-open a class and re-define its existing functionality. While some frown on this practice, it can be a powerful tool in your metaprogramming toolbelt. Be sure to use caution when monkeypatching.

# define original class
class Example
  def say_hello
    puts "hello"
  end
end

# re-open the class and monkeypatch some of its existing functionality
class Example
  # re-define existing funnctionality
  def say_hello
    puts "hello from monkeypatch"
  end
end

# usage
Example.new.say_hello # => hello from monkeypatch

Namespace

Use Ruby modules to create namespaces to avoid naming collisions.

# create a namespace to avoid naming collisions
module Example
  class String
    def length
      100
    end
  end
end

# usage
String.new.length # => 0
Example::String.new.length # => 100

Kernel Method

Defining methods in the Kernel module will make those methods available to all objects.

# add a kernel method to make it available to all objects
# this example also serves to illustrate that everything in Ruby is an object
module Kernel
  def say_hello
    puts "hello from #{self.class.name}"
  end
end

# usage
Class.say_hello # => hello from Class
Object.say_hello # => hello from Class
Object.new.say_hello # => hello from Object
1.say_hello # => hello from Fixnum
"".say_hello # => hello from String

Dynamic Dispatch

Ruby supports calling methods at runtime even if you don’t know what those methods are at design time.

# add methods that provide the ability
# to dynamically call unknown methods on objects
def invoke(object, method_name)
  object.send(method_name)
end

def invoke_with_args(object, method_name, *args)
  object.send(method_name, *args)
end

# usage
invoke("get my length", :length) # => 13
invoke("reverse me", :reverse) # => em esrever
invoke_with_args("remove all letter e's", :delete, "e") # => rmov all lttr 's

Pattern Dispatch

Similar to Dynamic Dispatch, but uses a convention or pattern to identify which methods to call.

# setup a contrived class to demonstrate pattern dispatch
class Person
  attr_accessor :first_name
  attr_accessor :last_name
  attr_accessor :pets_name
  attr_accessor :mothers_maiden_name

  def drag_queen_name
    "#{pets_name} #{mothers_maiden_name}"
  end
end

# construct an instance of the class
person = Person.new
person.first_name = "john"
person.last_name = "doe"
person.pets_name = "muffin"
person.mothers_maiden_name = "brown"

# use pattern dispatch to invoke all 'name' methods
person.public_methods.each do |method_name|
  puts "#{method_name} = #{person.send(method_name)}" if method_name =~ /_name$/
end

# -- output --
# first_name = john
# last_name = doe
# pets_name = muffin
# mothers_maiden_name = brown
# drag_queen_name = muffin brown

Dynamic Method

Ruby supports defining methods at runtime even if you don’t know what those methods are at design time.

# setup some data that will drive what methods get defined
$method_names = [:hello, :goodbye]

# define our example class
class Example
  # define some dynamic methods
  $method_names.each do |method_name|
    define_method(method_name) do |name|
      puts "#{method_name} #{name}!"
    end
  end
end

# test out our dynamic methods
Example.new.respond_to? :hello # => true
Example.new.respond_to? :goodbye # => true
Example.new.hello("nathan") # => hello nathan!
Example.new.goodbye("nathan") # => hello nathan!

Ghost Method

Ruby provides a mechanism that allows you to catch calls to methods that don’t even exist. Its possible to leverage this feature to support functionality that hasn’t been defined.

# define our example class
class Example
  # catch all calls to methods that don't exist
  def method_missing(method_name, *args)
    puts "You called '#{method_name}' with these arguments: #{args.inspect}"
  end
end

# invoke methods that haven't been defined
Example.new.some_method # => You called 'some_method' with these arguments: []
Example.new.another_method(1, 2, 3) # => You called 'another_method' with these arguments: [1, 2, 3]
Example.new.this_is_cool(true) # => You called 'this_is_cool' with these arguments: [true]
Example.new.and_powerful # => You called 'and_powerful' with these arguments: []

Dynamic Proxy

Wrapping an object or service and then forwarding method calls to the wrapped item is known as dynamic proxying.

# define our proxy class
class Proxy
  def initialize(object)
    @object = object
  end

  # forward all calls to the wrapped object
  def method_missing(method_name, *args)
    @object.send(method_name, *args)
  rescue
    puts "#{method_name} is not supported by the wrapped object!"
  end
end

# usage
Proxy.new("this is a string").length # => 16
Proxy.new([1, 2, 3]).length # => 3
Proxy.new({:a => 1, :b => 2}).length # => 2
Proxy.new(true).length # => length is not supported by the wrapped object!

Blank Slate

Ruby allows you to remove functionality from a class. This technique can be useful to ensure that your class doesn’t expose unwanted or unexpected features.

# demonstrate how to remove functionality
String.class_eval do
  undef_method :length
end
"test".length # => NoMethodError: undefined method `length' for "test":String

# create a blank slate class
class BlankSlate
  public_instance_methods.each do |method_name|
    undef_method(method_name) unless method_name =~ /^__|^(public_methods|method_missing|respond_to\?)$/
  end
end

# see what methods are now available
BlankSlate.new.public_methods # => ["public_methods", "__send__", "respond_to?", "__id__"]

Scope Gate

There are three ways to define a new scope in Ruby. A new scope is created whenever you define a class, module, or method.

Be aware that scoping in Ruby is different than some other languages. Ruby does not chain scopes when performing lookups, so don’t expect it to find variables defined in an outer scope.

# demonstrate scoping in ruby
scope = "main scope"
puts(scope) # => main scope

class ExampleClass
  # the main scoped variable isn't defined in the classes' scope
  defined?(scope) # => nil
  scope = "class scope"
  puts(scope) # => class scope
end

# the main scoped variable is unchanged by the classes' scoped variable
puts(scope) # => global scope

module ExampleModule
  # the main scoped variable isn't defined in the module's scope
  defined?(scope) # => nil
  scope = "module scope"
  puts(scope) # => module scope
end

# the main scoped variable is unchanged by the module's scoped variable
puts(scope) # => main scope

def example_method
  # the main scoped variable isn't defined in the method's scope
  defined?(scope) # => nil
  scope = "method scope"
  puts(scope)
end

example_method # => method scope

# the main scoped variable is unchanged by the method's scoped variable
puts(scope) # => global scope

Flat Scope

Flatten the scope to gain access to variables that are otherwise unaccessible.

# define a variable in the main scope
value = "sort of"

class Example
  attr_reader :read_only

  def initialize
    @read_only = true
    # the main scoped variable is not defined in this scope gate
    defined?(value) # => nil
  end
end

Example.new.read_only # => true

# flatten the scope with a closure (block) to share variables between scopes
# note that we are also violating encapsulation here
example.instance_eval do
  @read_only = value
end

example.read_only # => sort of

Shared Scope

Create a Scope Gate to share variables across several contexts.

# create a shared scope
shared_scope = Proc.new do
  # define a variable to share
  shared = "a shared variable"

  # use closures (blocks) to ensure access to the variable
  Example = Class.new do
    puts shared # => a shared variable

    # set a reference to the eigenclass so we can later define a class method
    # while retaining access to the shared variable
    eigenclass = class << self
      # this is a scope gate without access to the shared variable
      self
    end

    # use the eigenclass to define a class method
    # with access to the shared variable
    eigenclass.class_eval do
      define_method :class_method do
        shared
      end
    end

    # define an instance method with access to the shared variable
    define_method :instance_method do
      shared
    end
  end
end

# call the shared scope proc to execute its code
shared_scope.call

# the scoped variable 'shared' is not available to the main scope
defined?(shared) # => nil

# demonstrate the methods
Example.class_method # => a shared variable
Example.new.instance_method # => a shared variable

Context Probe

Ruby allows you to break the rules of encapsulation and reach into the internals of an object.

# define a sample class that we can probe
class Example
  def initialize
    @private = "this is a private instance variable"
  end
end

# send a context probe into an instance of the Example class
Example.new.instance_eval { puts @private } # => "this is a private instance variable"

Clean Room

A class or object used for the express purpose of evaluating Ruby inside of its context is called a clean room. Clean rooms are used to change the current context to something expected or clean which can help to avoid surprises.

def do_stuff
  @scope
end

@scope = "outer scope"
puts do_stuff # => outer scope

# illustrate how to use a simple clean room
Object.new.instance_eval do
  @scope = "clean room scope"
  puts do_stuff # => clean room scope
end

Deferred Evaluation


Class Instance Variable


Singleton Method


Class Macro


Class Extension


Object Extension


Around Alias


String of Code


Code Processor


Sandbox


Hook Method


Class Extension Mixin


Mimic Method


Lazy Instance Variable


Named Arguments


Argument Array


Self Yield


Symbol to Proc





comments powered by Disqus




comments powered by Disqus

Copyright © 2008-2013 Hopsoft