Made Tech Blog

Metaprogramming in Ruby

When you find yourself duplicating functionality that is similar to existing functionality in your system, it may be an indication that the process could be generalised to accommodate more scenarios. This has the benefit that this ‘role’ would be encapsulated and the source code would exist in only one place, becoming less of a maintenance burden.

As web developers, we may not write a lot of meta code in our day to day activities, but it is widely used in most systems. Compilers, parsers, assemblers, DSLs, web frameworks and preprocessors make heavy use of it. When used correctly, it can help you write more declarative code (as opposed to imperative) and help keep your codebase DRY.

The Factory Analogy

I find the factory analogy very effective when thinking about metaprogramming:

If you as a programmer had to build cars every day, you would soon be repeating the same process over and over. If you wanted to boost efficiency, and eliminate the risk of human error, you would build a factory to build the cars. Every execution of the car by the factory may be different as we pass in data for it to process, such as colour and make; but the role is broadly the same.

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves). You can also think of it as code that writes code.

– Wikipedia

As with most things, metaprogramming does come at a cost. The mental model increases significantly, and the ability to quickly scan and understand a codebase decreases.

The dynamic nature of Ruby makes it really easy to do metaprogramming. Rails relies heavily on metaprogramming to make the interface as intuitive as it is, without reams of code to catch every possible scenario.

A good example is the Dynamic finder methods in ActiveRecord like:

find_product_by_name_and_colour(name: name, colour: colour)

They are an example of how metaprogramming has made our lives much easier by implementing some complex internal logic elsewhere, out of sight. The public API exposed by metaprogramming is usually simplified, the implementation details are more complex.

This example is wonderfully illustrative of metaprogramming. There is no method called

find_product_by_name_and_colour

and instead is handled by a method_missing method up the call chain. Hitting a method_missing is intended use of the framework in this case, and ends up being executed by some dynamic receiver.

Associations are a set of macro-like class methods for tying objects together through foreign keys. Each macro adds a number of methods to the class which are specialised according to the collection or association symbol and the options hash.

– Comments in ActiveRecord::Relation

Open Classes In Ruby

a = "test"

def a.my_method
  puts "something"
end   

puts a.my_method  # => something

As you can see above, the “test” string instance now contains a new method called “my_method”. The String class (or the meta class of “test”) is not affected, and the my_method method is only valid for the duration of the instantiated string object.

Alternatively you could add the method to the String class by re-opening it and declaring the method on there. This is only one of many ways to dynamically add, or modify code at runtime. This is known as Monkey-patching, and is both powerful and dangerous at the same time. Many gems make use of this to add additional functionality to existing code.

Metaprogramming Concepts in Ruby

Dynamically defined methods

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

With this method in a class, you can use it to define methods on an existing object.

Self

self is a special variable that contains the current object at any time a method is called. If you don’t specify a receiver object for the method it will default to main, which is the top level object in Ruby. If you are calling a method from within an object, self will be the object. A method always executes on self, there are no exceptions.

Metaclass

Also known as singleton classes, the metaclass is an instance of the class Class. It is like an instance of the blueprint of the object. Every object in Ruby has one of these meta objects associated with it.

Contrary to Smalltalk 80, Ruby uses the Eigenclass. This is considered by some to be a design improvement, even though at first glance it seems more complicated. You can read more about Eigenclass on Wikipedia. This class is said to be one “meta-level” higher than than the object that you created.

Reflection

You can also use metaprogramming to do Reflection.

With reflection, a computer program is able to observe its own structure at runtime. This makes it useful for introspection, and can be used to gather information about the program itself.

An example of this is the .methods method in Ruby which will tell you which method an object responds to. Reflection can be used to make a program intuitive and increase its ‘knowledge’ about itself. Magic like __FILE__ and __LINE__, which contain internal knowledge about source code of the program can be helpful in building intuitive debugging messages.

Binding

The binding method on Kernel is really interesting. It is a container for the current local context in the method that it is in. This snapshot of the local variables in that scope, at that time, can be passed around in a variable and executed against other functionality elsewhere. The eval method takes this binding as an optional second argument when executing code.

Using Rails as an example again, it uses the binding object to execute view/layout related code in a certain context.

Inherited

When the inherited method is defined on an object, it will always be executed when it is subclassed. Functional programming has influenced object oriented programmers to prefer composition over inheritance, so if you find yourself using this a lot, it may be an indication that you are overcomplicating your design. Always prefer composition over inheritance.

Document Your Meta Code

It is generally frowned upon to have any comments in your code (because they can quickly become outdated), but I think that there is an exception for metaprogramming. You should obviously be testing your code, but adding a parsed version of the meta code for reference along with the test is also helpful for other developers.

I was very grateful to find a demonstration of a generated bit of example code in the Rails documentation, which instantly made it easier to understand.

People have different opinions on this. Just make sure that you are not creating something that is too complex without some form of documentation and a clear use case for it.

About the Author

Avatar for Emile Swarts

Emile Swarts

Lead Software Engineer at Made Tech

All about big beards, beers and text editors from the seventies.