Building VTS

The Official VTS Engineering Blog

Ruby Metaprogramming by Example

By Dean Nasseri

In Ruby the term metaprogramming refers to the dynamic nature of the language, which allows you to define and redefine methods and classes at runtime. Metaprogramming is often presented as a very nebulous and dangerous concept, one that is difficult to explain and hard to wrap your head around, and thus should be avoided. I’d like to to take some time to show a few powerful uses of metaprogramming techniques in real live code.

One of the most common, and most misudnerstood, aspects of ruby programming is the monkey patch. The monkey patch refers to the ability for ruby to dynamically define and override methods on existing classes and modules at runtime. For instance:

1
2
3
4
5
6
7
8
9
10
11
[1, 2, 3].to_s
=> "[1, 2, 3, 4]"

class Array
  def to_s
    '[]'
  end
end

[1, 2, 3].to_s
=> "[]"

This is admitedly not a very useful monkey patch, but it demonstrates that we can reach into any class, including core classes like Array, and modify it on the fly. The monkey patch is actually just a symptom of a more general paradigm in ruby.

Open Classes

In Ruby, class definitions are quite flexible.

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
  @color = 'red'
  puts @color
end
=> red
=> nil

class A
  @color = 'blue'
  puts @color
end
=> blue
=> nil

Ruby executes the body of the class definition as it would any other code. Interestingly the class keyword does not create the C class twice, which is what allows our earlier example of monkey patching to work. Take a look at the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# case 1
class B
  def method1
    1
  end
end




# case 2
class B
  def method2
    2
  end
end

B.new.method1
=> 1
B.new.method2
=> 2

In the first case, class B does not exist, so Ruby defines the class and method1. The second time class B is referenced, the class exists, so instead of redefining class B, it opens the existing class and defines method2 there. The method1 is still accessible because it is still the same class.

When opening a class we can even reference instance variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class C
  @color1 = 'red'
  puts @color1
end
=> red
=> nil

class C
  @color2 = 'blue'
  puts @color1
  puts @color2
end
=> red
=> blue
=> nil

Now its time to see some examples of metaprogramming in action. Lets take a look at the Awesome Print gem. Awesome Print is a gem for pretty printing Ruby objects. Awesome Print works with IRB as well as Pry, but for the time being, we will be focused on the IRB code path. For those that might now know IRB is the Ruby REPL distributed with MRI, and written in ruby. The repository can be found here: https://github.com/michaeldv/awesome_print.

To use AwesomePrint, first install the gem.

1
$ gem install awesome_print

Then in your ~/.irbrc

1
2
require "awesome_print"
AwesomePrint.irb!

Taking a look at the project we see that awesome_print.rb has two main files, inspector and formatter, and then a number of core extensions which do various monkey patches on core ruby classes. Lets take a look at the irb! method defined on the inspector class. This function is responsible for intercepting IRB output and formatting it appropriatley, in other words it is the “hook” into IRB’s output, intercepting what is outputted and replacing it with its own formatting logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# lib/awesome_print/inspector.rb (26)
def irb!
  return unless defined?(IRB)
  unless IRB.version.include?("DietRB")
    IRB::Irb.class_eval do
      def output_value
        ap @context.last_value
      end
    end
    ...
  end
end



# http://rxr.whitequark.org/mri/source/lib/irb.rb
def output_value # :nodoc:
  printf @context.return_format, @context.inspect_last_value
end

Looking through the IRB source on line 661 we see the method output_value. If we monkey patch it like so, check what happens:

1
2
3
4
5
6
7
8
9
module IRB
  class Irb
    def output_value
      puts 'cow'
    end
  end
end
'dog'
=> cow

So we can see that this is the method that prints to STDOUT. Returning back to the inspector irb! method, we see that this is not quite the same code as we had. Instead of opening up the class using the class keyword we use a class_eval.

1
2
3
4
5
IRB::Irb.class_eval do
  def output_value
    puts 'cow'
  end
end

The class_eval method allows us to execute code in the context of a class. This allows us to open up classes without the class keyword. We can’t have a class definition in a method body, so instead we can use class_eval, which allows us to monkey patch the output_value method dynamically. The class_eval method is very powerful, and allows us to completely redefine classes at runtime.

Lets take a look at another metaprogramming trick used in AwesomePrint. Open up lib/awesome_print/formatter.rb and take a look at the format method:

1
2
3
4
5
6
7
8
9
def format(object, type = nil)
  core_class = cast(object, type)
  awesome = if core_class != :self
    send(:"awesome_#{core_class}", object) # Core formatters.
  else
    awesome_self(object, type) # Catch all that falls back to object.inspect.
  end
  awesome
end

The format method is the main entry point to format an object. After determining the class of the object it is about to format, the method dynamically calls the corresponding formatter using send.

The final trick I’d like to show is located lib/awesome_print/core_ext/kernel.rb. This file contains extensions on the core Kernel class. This example comes from the book Metaprogramming Ruby by Paolo Perrota.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module Kernel

  def ai(options = {})
    ap = AwesomePrint::Inspector.new(options)
    awesome = ap.awesome self
    if options[:html]
      awesome = "<pre>#{awesome}</pre>"
      awesome = awesome.html_safe if defined? ActiveSupport
    end
    awesome
  end
  alias :awesome_inspect :ai

  def ap(object, options = {})
    puts object.ai(options)
    object unless AwesomePrint.console?
  end
  alias :awesome_print :ap

  module_function :ap
end

Here we can see that AwesomePrint has opened up the Kernel module, and is defining two methods ai and ap. Why define them on the Kernel module? Since Kernel is included in Object, and object is in the ancestor chain of nearly every Ruby class, defining a method within Kernel is a handy way of making it available nearly everywhere.

I hope you enjoyed this little intro on Ruby metaprogramming. To gain a deeper insight into Ruby metaprogramming, I highly suggest Palo Perrotta’s book which you can find here: https://pragprog.com/book/ppmetr2/metaprogramming-ruby-2.