The term metaprogramming can be translated into self-referential programming, which describes programming that can read, generate, analyze, and transform itself or another programming. Within Ruby, the concept and its application are often described as the language’s ability to dynamically define and redefine methods and classes at runtime, but it is not limited to this implementation. This ability to define methods and transform the program at runtime allows us to write dynamic code that avoids repetition and is often reusable. Metaprogramming is used within Ruby at every turn, and understanding the core concept of how Ruby uses it to create cleaner code is an essential part of becoming a more confident and well-rounded Ruby developer. Using some coding examples, I hope to simplify metaprogramming and shed some light on the ‘magic’ that happens when working with Ruby on Rails.
Getting and Setting: Under the Hood
Ruby on Rails gives developers access to modules full of methods and constants, and these methods, powered by Rails, are often the source of the supposed ‘magic’ that works for the developer behind the scenes. The methods attr_reader, attr_writer, and a method named attr_accessor that does the job of both the reader and writer methods are commonplace within even the most basic Ruby applications. These methods take up only single lines of code and allow users to access defined attribute variables associated with a given class.
In figure 1, the variables name, age, and breed are passed as arguments to the method attr_accessor for each new instance of the Cat class. What this method is doing under the hood is unseen, but in practice, this method call gives class access to instance variables named after the attributes passed to it and the corresponding methods used to ‘get’ and ‘set’ those variables. In figure 2, we can see what it would look like if we were to write that code ourselves:
The attr_accessor has saved us from writing out those definitions, but we are still in the dark regarding how this method was able to define them for us. We could dig deep into creating our own version of attr_accessor, and we would likely be making further use of the utility methods available to us within the Ruby modules, such as define_method. This aptly named method is used to define these methods for use on the fly. We would not be going any deeper under the hood, but we would be able to have some abstract idea of what the attr_accessor is doing, looping through each of the attributes it is passed and defining both ‘setter’ and ‘getter’ methods for them at runtime. Figure 3 displays our own version of that attr_accessor method, using even more ‘magic’ utility methods in the form of instance_variable_get and instance_variable_set. These methods allow us to get and set variables we do not know the name of, which creates some very dynamic code that could be used in any application.
As the output shows in figure 3.5, this function works just as well as attr_accessor. Even without diving further into the raw code that would explain what these methods are doing, we have an idea of what is happening, thanks to the names of the methods we used. Ruby is a language where a lot of these methods are named almost perfectly to describe their purpose, like define_method. They can be intuitive to work with us we can find methods that are ideal for the task at hand, just by glancing at the name. Most of these methods have been designed to utilize metaprogramming to generate lines of code behind the scenes, making Ruby code, on its face, more modular, dynamic, and DRY.
The Catch-All Bucket
Another amazing method Ruby has available to developers is method_missing, which is used to route any method calls that are undefined within the class in question. Undefined method calls, along with their arguments and blocks, as passed as arguments to method_missing, where we can customize and create a catch-all handler. We can use this method to gracefully print a custom error for these method calls, as shown in figure 4 below:
Instead of returning this custom error, we could define behavior within method_missing that can change the way our class responds to undefined methods. Figure 5 is taken from the official documentation for method_missing, where we are given a fantastic example of where method_missing has been customized to translate Roman numerals into integers. In this implementation, the developer does not need to call the roman_to_int method, which they have defined themselves, and instead can directly pass the undefined method as an argument to roman_to_int. We use another method, named id2name, to convert that method name into a string, as required by roman_to_int. This custom method will then translate that string, as displayed in figure 5, in the method calls and their outputs:
Without the use of method_missing, calling undefined methods on our instance would bring up the NoMethodError, stating that the method we tried to call was undefined for the instance object in question. As such, method_missing has transformed the expected behavior for this class and allowed us to call undefined methods. It could be said that this functionality is too broad as it does not account for methods that might not translate well into Roman numerals, so there are limits to the implementation of these catch-all techniques. Developers need to understand and consider the negative possibilities, and at least handle them before implementing these metaprogramming methods.
Introspection in Ruby
We can use metaprogramming to dynamically change our program, but we can also apply the concept in much simpler ways. The practice of calling methods to help us learn about our code is referred to as introspection in Ruby. In figure 6 below, calling the inspect method will return the object — the Tabby instance of class Cat with the name “Joe” — in a readable form.
This simple action, and the many other methods that do not transform our code, still use the state of our program to resolve their action, and because of this, they fall within the umbrella of metaprogramming. We can use introspection to determine what type of class something is when calling methods like class.name, which returns the name of the class as a string. Instance_of? is a method that takes in a class name as an argument, determining if the instance object it is called on is an instance of the given class name. In figure 7, we see some further introspection regarding the Cat class and the instance Tom. This example reveals just how simple metaprogramming can be.
Making use of these methods of introspection allows us to have a greater understanding of our program. We can delve into the classes to determine information regarding what methods they have available with methods like methods.inspect. We still use the inspect method but direct the inspection to reveal all methods associated with that class, returned within an array. The results from this method are not always returned in an easily readable form, as shown in figure 8, so using puts with these methods is not always recommended. To check if a class can respond to a given method, we can use respond_to? which takes in a method name as an argument, and returns a boolean based on whether that method name is defined for that class or not. Again, most methods in Ruby have incredibly intuitive names, which makes our lives easy when determining which methods to use.
Introspection is an amazing concept used to check the state of our program and classes. These methods can be useful when debugging, revealing what classes, methods, and instance variables we have available to us within our program. Using introspection will help produce cleaner code through our enhanced knowledge of our program and its parts.
Understanding the depth of methods available will help Ruby developers write significantly less code when implementing their features because we use built-in utility offered by single lines of code. Even if the understanding of these methods is only in the abstract, having some idea about what will occur during each method call and thinking about that expected result will improve how we approach Ruby code. Improving our ability to identify how our program will be read, generated, analyzed, or transformed will allow us to fully utilize metaprogramming. It may seem like ‘magic’ is occurring behind the scenes, but we are simply taking advantage of the tools and innovations made by previous generations of Ruby developers. We can use those tools and advantages to push further with innovation, paving the way for the generation of Ruby developers to come after us.
Metaprogramming is taking place all the time when developing with Ruby, and many other languages and frameworks as well. When programs treat their own programming as their data, we can create functionality that transforms itself while running. The ability to write code that is incredibly dynamic and modular allows for re-usability and allows developers to spend more time spent on further functionality. Code that has those characteristics is something that can be considered ideal in the tech industry, and it is metaprogramming that allows this ideal software and code to exist.