Metaprogramming Ruby

I Love the book “Metaprogramming Ruby” by Paolo Perrotta and found it very informative. The idioms defined in the book are so helpful. Here I have created a reference based on them for my own use. Hopefully it will help others too.

1) Dynamic Dispatch
Ruby allows us to dynamically call unknown methods(even private methods) on objects.

# object.send(message, *arguments)
2.send(:+, 3) # => 5

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

class User
  attr_accessor :first_name
  attr_accessor :last_name

  def full_name
    "#{first_name} #{last_name}"
  end
end

user = User.new
user.first_name = "Ranjithkumar"
user.last_name = "Ravi"

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

# -- output --
# first_name = Ranjithkumar
# last_name = Ravi
# full_name = Ranjithkumar Ravi

3) Dynamic Method
Ruby allows us to dynamically create methods at runtime

class Bar
  def self.create_method(method)
    define_method "my_#{method}" do
      puts "Dynamic method called 'my_#{method}'"
    end
  end

  # these methods are executed within the definition of the Bar class
  create_method :foo
  create_method :bar
end

# Test out our dynamic methods
Bar.new.respond_to? :my_foo # => true
Bar.new.my_foo # => "Dynamic method called 'my_foo'"
Bar.new.my_bar # => "Dynamic method called 'my_bar'"

4) Ghost Method
When a method is not found, Ruby will send this method as a symbol to method_missing.

class Example
  def method_missing(method_name, *args)
    puts "You called: #{method_name}(#{args.join(', ')})"
    puts "You also passed a block" if block_given?
  end
end

Example.new.this_is_cool(1, 2, 3) # => You called: this_is_cool(1, 2, 3)   
Example.new.this_is_cool(:a, :b, :c) { puts "a block" } # => You called: this_is_cool(a, b, c)
                                                        # => You also passed a block 

5) Dynamic Proxy
Wrapping an object or service and then forwarding method calls to the wrapped item is known as dynamic proxying. In other word, Catching Ghost Method and forwarding them onto another method/service.

def method_missing(method_name, *args, &block)
  return get($1.to_sym, *args, &block) if method_name.to_s =~ /^get_(.*)/
  super # if we don't find a match then we'll call the top level `BasicObject#method_missing`
end

If we find a match for get_#{name} then we will delegate to another method such as get(:data_type) where :data_type is :name or :age(e.g. get_name, get_age etc) else send to super and raise error.

6) Blank Slate
Ruby allows us to remove functionality from a class. This technique can be useful to ensure that your class doesn’t expose unwanted or unexpected features.
e.g. Prevents issues when using “Dynamic Proxy”. User calls a method that exists higher up the inheritance chain so your method_missing doesn’t fire because the method does exist. To work around this issue, make sure your class starts with a “Blank Slate”.
To remove method, use Module#undef_method (removes all the methods), or Module#remove_method (remove receiver’s method, keep inherited methods). Ghost methods are slower than normal methods. Do not remove methods start with __, method_missing or respond_to?, and leave some other methods.

# create a blank slate class
class ImBlank
  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
ImBlank.new.public_methods # => ["public_methods", "__send__", "respond_to?", "__id__"]

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

module Kernel
  def say_hello
    puts "hello from #{self.class.name}"
  end
end

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

8) Scope
Class.new is an alternative to class
Scope Gate:
There are 3 ways to define a new scope in Ruby:

  • starting new class definition, class
  • starting new module definition, module
  • start new method, def

Global variable can access any scope. 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.

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

Flattening the scope:
Where you change the code in such a way that it’s easier for you to pass variables through “Scope Gates”.

my_var = "abc"
MyClass = Class.new do
  puts "#{my_var} in class"

  define_method :my_method do
    puts "#{my_var} in method"
  end
end # => abc in class

MyClass.new.my_method # => abc in method

9) Eigenclass
A hidden class on the ancestors chain. Eigenclass is a singleton class object. It stores the singleton method of an object.
Class Extension:

class MyClass
  class << self
    def my_method; 'hello'; end
  end
end

MyClass.my_method # => "hello"

Object Extension:

module MyModule
  def my_method; 'hello'; end
end

obj = Object.new
class << obj # extends obj
  include MyModule
end

obj.my_method # => "hello"
obj.singleton_methods # => [:my_method]

# Another way to extend object

obj = Object.new
obj.extend MyModule
obj.my_method # => "hello"

10) Context Probe
Execute a code block in the context of another object using instance_eval

class Foo
  def initialize
    @z = 1
  end
end
foo = Foo.new
foo.instance_eval do
  @z = 2
  puts @z # => 2
end

# There is also `instance_exec` which works the same way but allows passing arguments to the block
Foo.new.instance_exec(3) { |arg| @z * arg } # => 3

11) Clean Room
Clean rooms are used to change the current context to something expected or clean (does not affect to current environment).

def do_stuff
  @scope
end

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

Object.new.instance_eval do
  @scope = "clean room scope"
  puts do_stuff # => clean room scope
end

12) class_eval
Evaluate a block in the context of a class. Similar to re-opening a class but more flexible in that it works on any variable that references a class, where as re-opening a class requires defining a constant.

def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
'abc'.m # => "Hello!"

13) Class Macro
Class Macros are just regular class methods that are only used in a class definition.
e.g. attr_accessor, attr_reader. These are class macros.


Write your own class macro. Here is an example of deprecate old methods, print warning message when being called.
class Book
  def self.title; puts "I'm an A" end

  def self.deprecate(old_method, new_method)
    warn "Warning: #{old_method}() is deprecated. Use #{new_method}()"
    send(new_method)
  end

  deprecate :GetTitle, :title
end

14) Around Alias
Around Alias uses the alias keyword to store a copy of the original method under a new name, allowing you to redefine the original method name and to delegate off to the previous method implementation.

class String
  alias :orig_length :length # make alias of old method

  def length # define new method, override
    "Length of string '#{self}' is: #{orig_length}"
  end
end

"abc".length #=> "Length of string 'abc' is: 3"

15) Hook Methods
The method being called when event triggered, like Module#included, Class#inherited

class String
  def self.inherited(subclass)
    puts "#{self} was inherited by #{subclass}"
  end
end
class MyString < String; end # => String was inherited by MyString

Method-related hooks:

  • method_missing
  • method_added
  • method_removed
  • method_undefined
  • singleton_method_added
  • singleton_method_removed
  • singleton_method_undefined

Class & Module hooks:

  • inherited
  • included
  • extended
  • extend_object
  • const_missing
  • append_features
  • initialize_copy

Marshalling hooks:

  • marshal_dump
  • marshal_load

16) Class Extension Mixin
Class Extension Mixin allows you to both include and extend a class

module MyMixin
  def self.included(base) # Hook Method
    base.extend ClassMethods
  end

  # Class Methods
  module ClassMethods
    def x
      puts "I'm X (a class method)"
    end
  end

  # Instance Methods
  def a
    puts "I'm A (an instance method)"
  end
end

class Foo
  include MyMixin
end

Foo.x # => I'm X (a class method)
Foo.new.a # => I'm A (an instance method)

Comments