Understanding Ruby Metaprogramming

by Diego Mónaco

Foundations

  • Everything is an object
  • Who is self?
  • Method lookup

Everything is an Object


  nil.class # => NilClass
  nil.object_id # => 4

  false.class # => FalseClass
  false.object_id # => 0

  true.class # => TrueClass
  true.object_id # => 2

  3.class # => Fixnum
  3.object_id # => 7

  Foo = Class.new

  Foo.class # => Class
  Foo.object_id # => 75217210
            

Class objects

  • Classes are just objects with the ability to define instance methods on their body, and spawn new objects
  • An instance of a class called Class is created the first time you open a class
  • The name of a Class is just a constant pointing to a Class object
  • All classes can be reopened

The following snippets are equivalent:


  class Foo
    def foo
    end
  end

  Foo = Class.new do
    def foo
    end
  end
            

Re opening classes


 class Foo
   def method_one
   end
 end

 Foo.instance_methods(false) # => [:method_one]

 class Foo
   def method_two
   end
 end

 Foo.instance_methods(false) # => [:method_one, :method_two]
            

Re opening NilClass


  class NilClass
    def wtf!
      puts 'this is crazy'
    end
  end

  nil.wtf! # => this is crazy
            

Re opening Class


  class Class
    def chatty_new
      puts "Hi, I'm about to create a new object!"

      new
    end
  end

  class Bar; end
  class Foo; end

  Bar.chatty_new # => Hi, I'm about to create a new object! 
                 # => #<Bar:0x909cd0c>

  Foo.chatty_new # => Hi, I'm about to create a new object! 
                 # => #<Foo:0x924c8c8>
            

What about Modules?

  • Modules are just like Classes but can't instantiate objects
  • A Class is a Module, but can't be mixed-in

Don't trust me let's ask ruby


  class Foo; end

  Foo.is_a? Module # => true

  Class.superclass #  => Module

  Class.instance_methods(false) # => [:allocate, :new, :superclass]

  module M; end

  M.respond_to? :new # => false
            

Who is self?

  • In ruby you are always inside an object and 'self' is the current object
  • Any method call without an explicit receiver will be sent to 'self'
  • There is always one (and only one) object playing the role of 'self' at a given time
  • The role of 'self' will be played by different objects depending on context
  • Is the owner of instance variables

A different 'self' for each context


  # Top Level Context
  puts self # => main

  class Foo
    # Class Body Context
    puts self # => Foo

    module M
      # Module Body context
      puts self # => Foo::M
    end

    def a_method
      # Method Body Context
      puts self
      another_method
    end

    def another_method
      puts self
    end
  end

  Foo.new.a_method # => #<Foo:0x471004>
                   # => #<Foo:0x471004>
  Foo.new.a_method # => #<Foo:0x34c566>
                   # => #<Foo:0x34c566>
            

Instance variable ownership


  class Foo
    @foo_iv = "Foo_iv"

    def initialize(iv_value)
      @foo_iv = iv_value
    end
  end

  Foo.instance_variable_get :@foo_iv # => "Foo_iv"

  Foo.new("foo_1_iv").instance_variable_get :@foo_iv # => "foo_1_iv"
  Foo.new("foo_2_iv").instance_variable_get :@foo_iv # => "foo_2_iv"
            

Instance variables in the top level context


  # Top Level context

  @main_iv = "main_iv"

  m = self # => main

  m.instance_variable_get :@main_iv # => "main_iv"
            

Method lookup

  • All methods are defined in classes or modules
  • Objects receive messages and execute methods
  • They find them by searching through classes and modules
  • The search ends with the method execution when it is found or with an error if it isn't found
  • The list of classes and modules where the search is performed is called the 'method lookup path'
  • The 'method lookup path' is defined by the ancestors chain of the object's class
  • The class Object is the default superclass

Sending messages


  "abcde".public_send(:upcase) # => ABCDE

  "abcde".public_send(:chomp, "de") # => abc
            

Breaking encapsulation


  class Foo

    private

    def a_private_method
      "you should not be calling me from the ouside world"
    end

  end

  foo = Foo.new
  foo.send(:a_private_method) # => you should not be calling me from the ouside world

            

Method Lookup Rule:

"one step to the right, then up"

Go one step to the right into the receiver's class, and then up the ancestors chain, until you find the method

Riding the ancestors chain with 'super'


  module M
    def hello
      puts 'Hi from M'
    end
  end

  class Bar
    include M

    def hello
      puts 'Hi from Bar'
      super
    end
  end

  class Foo < Bar
    def hello
      puts 'Hi from Foo'
      super
    end
  end

  foo = Foo.new

  foo.hello  # Hi from Foo
             # Hi from Bar
             # Hi from M

  foo.class.ancestors # => [Foo, Bar, M, Object, Kernel, BasicObject]
            

  [Foo, Bar].each { |c| c.send(:remove_method, :hello) }

  foo.hello # => Hi from M
            

modules are searched in reverse orden of inclusion


  module M; end

  module N; end

  class A
    include M
    include N
  end

  A.ancestors # => [A, N, M, Object, Kernel, BasicObject]
            

The Ghost class

  • Better known as Eigenclass, Metaclass or Singleton class
  • Is an anonymous class
  • For all practical purposes is a class like any other
  • Is where per object behavior gets defined
  • Is always the first class in the 'method lookup path'

Defining singleton methods


  o = Object.new

  def o.bar
    "Hi from o's singleton class"
  end

  puts o.bar # => Hi from o's singleton class

  x = Object.new

  x.bar # => NoMethodError: undefined method `bar' for #<Object:0x909cd0c> 

  class << x
    def bar
      "Hi from x's singleton class"
    end
  end

  x.bar # => Hi from x's singleton class
            

The Eigenclass superclass


  class A
  end

  a = A.new

  a.singleton_class.superclass # => A
            

Including a module in a singleton class


  module N
    def baz
      puts 'Hi from N'
    end
  end

  z = Object.new

  class << z
    include N
  end

  z.baz # => 'Hi from N'
            

class methods: ordinary methods defined in the 'singleton class' of the given class instance


  class A
    def self.one
      'one'
    end

    def A.two
      'two'
    end

    class << self
      def three
        'three'
      end
    end
  end

  A.one   # => one
  A.two   # => two
  A.three # => three

  A.singleton_class.instance_methods(false) # => [:one, :two, :three]
  A.singleton_class.ancestors # => [Class, Module, Object, Kernel, BasicObject]
            

Class methods inheritance


  class A
   def self.a_class_method
     "Hi from A"
   end
  end

  B = Class.new(A)

  B.a_class_method # => Hi from A
            

Here is a tongue-twister:

"The superclass of the eigenclass of a regular object is the object’s class"

"The superclass of the eigenclass of a class object is the eigenclass of the class’s superclass"

Class methods inheritance


  class Module
    def foo
      puts 'Hi from Module'
    end
  end

  class Class
    def foo
      puts 'Hi from Class'
      super
    end
  end

  class Object
    def self.foo
      puts 'Hi from Object eigenclass'
      super
    end
  end

  class A
    def self.foo
      puts 'Hi from A eigenclass'
      super
    end
  end

  class B < A
    def self.foo
      puts 'Hi from B eigenclass'
      super
    end
  end


  B.foo  # => Hi from B eigenclass
         # => Hi from A eigenclass
         # => Hi from Object eigenclass
         # => Hi from Class
         # => Hi from Module
            

What happened?


  klass = B.singleton_class

  while klass.superclass
    puts klass
    klass = klass.superclass
  end

#<Class:B> B eigenclass
#<Class:A> A eigenclass
#<Class:Object> Object eigenclass
#<Class:BasicObject> BasicObject eigenclass
Class
Module
Object
            

Show me the metaprogramming!

Wait, wtf is metaprogramming!

Metaprogramming: "code that writes code"

Euclidean meta-geometry



module Geometry
  FORMULAS = {
    triangle: {perimeter: 'l1+l2+l3', area: 'b*h/2'},
    rectangle: {perimeter: 'l1*2+l2*2', area: 'l1*l2'},
    circle: {perimeter: '2*3.14*r', area: '3.14*r*r'}
  }
end

Geometry::FORMULAS[:triangle][:perimeter]
=> "l1+l2+l3"
          

Let's get user friendly



Geometry.triangle_area
# => "b*h/2"

Geometry.circle_perimeter
# => "2*2.14*r"

            


module Geometry
  FORMULAS = {
    triangle: {perimeter: 'l1+l2+l3', area: 'b*h/2'},
    rectangle: {perimeter: 'l1*2+l2*2', area: 'l1*l2'},
    circle: {perimeter: '2*3.14*r', area: '3.14*r*r'}
  }

  def self.triangle_perimeter
    Geometry::FORMULAS[:triangle][:perimeter]
  end

  def self.triangle_area
    Geometry::FORMULAS[:triangle][:area]
  end

  def self.rectangle_perimeter
    Geometry::FORMULAS[:rectangle][:perimeter]
  end

  # def self.rectangle_area

  # def self.circle_perimeter

  # etc, etc
end
            

We are lazy


module Geometry
  FORMULAS = {
    triangle: {perimeter: 'l1+l2+l3', area: 'b*h/2'},
    rectangle: {perimeter: 'l1*2+l2*2', area: 'l1*l2'},
    circle: {perimeter: '2*3.14*r', area: '3.14*r*r'}
  }

  class << self
    def triangle_perimeter
      Geometry::FORMULAS[:triangle][:perimeter]
    end

    def triangle_area
      Geometry::FORMULAS[:triangle][:area]
    end

    def rectangle_perimeter
      Geometry::FORMULAS[:rectangle][:perimeter]
    end

    # def rectangle_area

    # def circle_perimeter

    # etc, etc
  end
end

          

We are Really lazy


module Geometry
  FORMULAS = {
    triangle: {perimeter: 'l1+l2+l3', area: 'b*h/2'},
    rectangle: {perimeter: 'l1*2+l2*2', area: 'l1*l2'},
    circle: {perimeter: '2*3.14*r', area: '3.14*r*r'}
  }

  class << self
    FORMULAS.each do |figure, formulas|
      formulas.each do |name, expression|
        define_method "#{figure}_#{name}" do
          expression
        end
      end
    end
  end
end

Geometry.circle_area
# => "3.14*r*r"
Geometry.singleton_methods
# => [:triangle_perimeter, :triangle_area, :rectangle_perimeter, :rectangle_area, :circle_perimeter, :circle_area]
            

define_method


# From: proc.c (C Method):
# Number of lines: 30
# Owner: Module
# Visibility: private
# Signature: define_method(*arg1)

# Defines an instance method in the receiver. The _method_
# parameter can be a Proc, a Method or an UnboundMethod object.
# If a block is specified, it is used as the method body. This block
# is evaluated using instance_eval, a point that is
# tricky to demonstrate because define_method is private.
# (This is why we resort to the send hack in this example.)

   class A

     def fred
       puts "In Fred"
     end

     def create_method(name, &block)
       self.class.send(:define_method, name, &block)
     end

     define_method(:wilma) { puts "Charge it!" }
   end

   class B < A
     define_method(:barney, instance_method(:fred))
   end

   a = B.new
   a.barney
   a.wilma
   a.create_method(:betty) { p self }
   a.betty

# produces:

#    In Fred
#    Charge it!
   #<B:0x401b39e8>
            

Hey! we can use classes!


class Rectangle
  attr_reader :l1, :l2

  FORMULAS = {perimeter: 'l1*2+l2*2', area: 'l1*l2'}

  def initialize(attrs)
    @l1, @l2 = attrs[:l1], attrs[:l2]
  end

  def perimeter
    l1*2+l2*2
  end

  def area
    l1*l2
  end

  FORMULAS.each do |name, expression|
    define_method "#{name}_formula" do
      expression
    end
  end
end

r = Rectangle.new(l1:3, l2:4)
r.area # => 12
r.area_formula # => l1*2+l2*2
              

But :(


# class Triangle
#   blah, blah

# class Circle
#   blah, blah

# class Whatever
#   blah, blah

#   etc, etc

              

Let's dream


Rectangle = Euclides.define :rectangle,
                            [:l1,:l2],
                            perimeter: 'l1*2+l2*2',
                            area: 'l1*l2'

Triangle  = Euclides.define :triangle,
                            [:l1,:l2,:l3,:b,:h],
                            perimeter: 'l1+l2+l3',
                            area: 'b*h2'

Circle    = Euclides.define :circle,
                            [:r],
                            perimeter: '2*3.14*r',
                            area: '3.14*r*r'
            


module Euclides
  class << self
    def define(figure, attributes, formulas)
      Class.new do
        const_set('FORMULAS', formulas)

        attr_reader *attributes

        define_method :initialize do |attrs|
          attributes.each do |attr|
            instance_variable_set("@#{attr}", attrs[attr])
          end
        end

        const_get('FORMULAS').each do |name, expression|
          define_method "#{name}_formula" do
            expression
          end

          define_method name do
            eval expression
          end
        end

      end
    end
  end
end
            


r = Rectangle.new l1:3, l2:5
r.l1 # => 3
r.area_formula # => l1*l2
r.area # => 15
Rectangle::FORMULAS
# => {:perimeter=>"l1*2+l2*2", :area=>"l1*l2"}
            

const_set


# From: object.c (C Method):
# Number of lines: 6
# Owner: Module
# Visibility: public
# Signature: const_set(arg1, arg2)

# Sets the named constant to the given object, returning that object.
# Creates a new constant if no constant with the given name previously
# existed.

   Math.const_set("HIGH_SCHOOL_PI", 22.0/7.0)   #=> 3.14285714285714
   Math::HIGH_SCHOOL_PI - Math::PI              #=> 0.00126448926734968
            

instance_variable_set


# From: object.c (C Method):
# Number of lines: 14
# Owner: Kernel
# Visibility: public
# Signature: instance_variable_set(arg1, arg2)

# Sets the instance variable names by symbol to
# object, thereby frustrating the efforts of the class's
# author to attempt to provide proper encapsulation. The variable
# did not have to exist prior to this call.

   class Fred
     def initialize(p1, p2)
       @a, @b = p1, p2
     end
   end

   fred = Fred.new('cat', 99)
   fred.instance_variable_set(:@a, 'dog')   #=> "dog"
   fred.instance_variable_set(:@c, 'cat')   #=> "cat"
   fred.inspect                             #=> "#<Fred:0x401b3da8 @a=\"dog\", @b=99, @c=\"cat\">
            

const_get


# From: object.c (C Method):
# Number of lines: 6
# Owner: Module
# Visibility: public
# Signature: const_get(*arg1)

# Returns the value of the named constant in mod.

   Math.const_get(:PI)   #=> 3.14159265358979

If the constant is not defined or is defined by the ancestors and
inherit is false, NameError will be raised.
            

eval


# From: vm_eval.c (C Method):
# Number of lines: 12
# Owner: Kernel
# Visibility: private
# Signature: eval(*arg1)

# Evaluates the Ruby expression(s) in string. If
# binding is given, which must be a Binding
# object, the evaluation is performed in its context. If the
# optional filename and lineno parameters are
# present, they will be used when reporting syntax errors.

   def getBinding(str)
     return binding
   end
   str = "hello"
   eval "str + ' Fred'"                      #=> "hello Fred"
   eval "str + ' Fred'", getBinding("bye")   #=> "bye Fred"
            

Let's get fancy


Triangle  = Euclides.define_triangle [:l1,:l2,:l3,:b,:h],
                                     perimeter: 'l1+l2+l3',
                                     area: 'b*h2'

Circle    = Euclides.define_circle [:r],
                                   perimeter: '2*3.14*r',
                                   area: '3.14*r*r'

Fooaedro  = Euclides.define_fooaedro [:foo, :bar, :baz],
                                     foometer: '3*foo+bar',
                                     barea: '12.4*foo/bar',
                                     bazity: 'baz*0.56'
            


module Euclides
  class << self
    def define(figure, attributes, formulas)
      Class.new do
        const_set('FORMULAS', formulas)

        attr_reader *attributes

        define_method :initialize do |attrs|
          attributes.each do |attr|
            instance_variable_set("@#{attr}", attrs[attr])
          end
        end

        const_get('FORMULAS').each do |name, expression|

          define_method "#{name}_formula" do
            expression
          end

          define_method name do
            eval expression
          end

        end
      end
    end

    def method_missing(method_name, *args)
      if method_name.to_s =~ /^define_(.*)$/
        public_send(:define, $1.to_sym, *args)
      else
        super
      end
    end

  end
end

            


f=Fooaedro.new foo:23, bar:34, baz:12.5
# => #<Fooaedro:0x9bcb78c @bar=34, @baz=12.5, @foo=23>
f.foometer
# => 103
f.barea
# => 8.388235294117647
f.bazity
# => 7.000000000000001
Fooaedro::FORMULAS
# => {:foometer=>"3*foo+bar", :barea=>"12.4*foo/bar", :bazity=>"baz*0.56"}
f.foometer_formula
# => "3*foo+bar"
            

method_missing


# From: vm_eval.c (C Method):
# Number of lines: 27
# Owner: BasicObject
# Visibility: private
# Signature: method_missing(*arg1)

# Invoked by Ruby when obj is sent a message it cannot handle.
# symbol is the symbol for the method called, and args
# are any arguments that were passed to it. By default, the interpreter
# raises an error when this method is called. However, it is possible
# to override the method to provide more dynamic behavior.
# If it is decided that a particular method should not be handled, then
# super should be called, so that ancestors can pick up the
# missing method.
# The example below creates
# a class Roman, which responds to methods with names
# consisting of roman numerals, returning the corresponding integer
# values.

   class Roman
     def romanToInt(str)
       # ...
     end
     def method_missing(methId)
       str = methId.id2name
       romanToInt(str)
     end
   end

   r = Roman.new
   r.iv      #=> 4
   r.xxiii   #=> 23
   r.mm      #=> 2000
            

public_send


# From: vm_eval.c (C Method):
# Number of lines: 5
# Owner: Kernel
# Visibility: public
# Signature: public_send(*arg1)

# Invokes the method identified by _symbol_, passing it any
# arguments specified. Unlike send, public_send calls public
# methods only.

   1.public_send(:puts, "hello")  # causes NoMethodError
            

See also:

  • class_eval
  • instance_eval
  • .....
  • Hooks:
    • included
    • extended
    • inherited
    • ....

Explore

Mix and Match

Use your imagination

Gracias!