FullStack Labs

Please Upgrade Your Browser.

Unfortunately, Internet Explorer is an outdated browser and we do not currently support it. To have the best browsing experience, please upgrade to Microsoft Edge, Google Chrome or Safari.
Upgrade

Basis of Meta-Programming in Ruby

Written by 
Hugo Rincon
,
Senior Software Engineer
Basis of Meta-Programming in Ruby
blog post background
Recent Posts
Google AMP vs. Other Frameworks - When to Use Each
How to Create a Simple File-Transfer WebRTC React Web Application
Using Split Flags with React to Control Feature Deployments

Metaprogramming is a programming technique in which programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running.

With this in mind, I am going to introduce you to the main methods and tools that Ruby offers us to focus on metaprogramming.

Table of contents

The Ruby send method (info)

Possibly the method most related to metaprogramming that Ruby offers is the ability to run a method with a symbol or string:

	
send(symbol [, args...]) → obj
	

First, let's set a problem that can be optimized with the send method:

	
class ProductClassifier
  attr_accessor :products

  def initialize(products)
    @products = products
  end

  def classify
    products.each do |product|
      case product.status
      when 'lost'
        lost_product_handler(product)
      when 'found'
        found_product_handler(product)
      when 'damaged'
        damaged_product_handler(product)
      when 'incorrect_location'
        incorrect_location_handler(product)
    end
  end

  private

  def lost_product_handler(product)
    # remove lost product from the inventory
  end

  def found_product_handler(product)
    # include found product in the inventory
  end

  def damaged_product_handler(product)
    # mark product to physical review
  end

  def incorrect_location_handler(product)
    # move the product to the right location
  end
end
	

In this case, given that for each status we have totally different actions, we find ourselves in need of establishing a case statement to handle each possibility. In this example, we only have four cases, but in any program that we are developing, we might need to have many more cases where it would be very complicated.

There is where send comes into play. Let's refactor that code using the send method:

	
class ProductClassifier
  attr_accessor :products

  def initialize(products)
    @products = products
  end

  def classify
    Products.each{ |p| self.send("#{p.status}_product_handler", p) }
  end
...
	

This way we can reduce a huge amount of lines of code, making it much easier to understand. This also leads us to a possible problem, which is that if for some reason the status of the product can be manipulated, we could call send with a method that does not exist. That is where method_missing comes into play.

Defining the `method_missing` (info)

This method is executed by Ruby automatically when we call a method that does not exist or has not been defined:

	
method_missing(symbol [, *args] ) → result
	

Continuing with our example, suppose we have a new product with an unknown status for our system --- let's say 'in_review' --- that would throw the following error:

	
NoMethodError (undefined method `in_review_product_handler' for ProductClassifier:Class)
	

A possible solution to this case is to define `method_missing` in our class and let the user know that the system does not know how to act with this status in this way:

	
class ProductClassifier
  attr_accessor :products
  ...


  private

  ...

  def method_missing(method_name, *args)
    # here we can check the method_name
    # and args and decide what to do,
    # we could have a common method for
    # undefined statuses or simply notify the user
  end
end
	

Another way to handle this type of problem is with define_method, which would allow us to define an answer without getting the error of the undefined method. We will explain this in the next section, the define_method.

Creating methods at runtime with `define_method` (info)

The define_method is used to create methods at runtime.

	
define_method(symbol, method) → symbol
	

Using this method, we can define a method and generate the response we want before falling into an error generated by Ruby, which we can see in this example:

	
class ProductClassifier
  attr_accessor :products

  def initialize(products)
    @products = products
    check_statuses
  end

  ...

  private

  def check_statuses
    products.each do |p|
      # check if the method is not defined in the class
      unless self.respond_to?("#{p.status}_product_handler", true)
        # we define the new method
        self.class.define_method("#{p.status}_product_handler") do |p|
          puts "We still don't have a handler for status '#{p.status}'"
        end
      end
    end
  end
  ...
	

This gives us infinite possibilities to manage our data in our application creating methods in the moments that we need them following some pattern that we define for our application.

The Metaclasses - extra bonus

This is more like an extra bonus. Something curious about Ruby is that each object has its own metaclass. In this example, we are going to manipulate them:

	
data_string = "Name, email@test.com, +112345678"

def data_string.as_array
  self.split(', ')
end

# this way we can call data_string.as_array and get an array with the data
data_string.as_array
=> ["Name", "email@test.com", "+112345678"]
	

This is called a singleton method and essentially the only difference with a class method is that it only affects the instantiated object that we apply it to, and this is another way to write it: 

	
data_string = "Name, email@test.com, +112345678"

# looks a little different but is just the same
class << data_string
  def as_array
    self.split(', ')
  end
end
	

Conclusion 

Knowing these methods and techniques that Ruby offers us, we can greatly expand the range of possibilities when developing our applications. Applying this technique, we will usually be able to significantly reduce the amount of code we need to reach the same results or better.
You can learn more about our custom software development capabilities by navigating through our site and blog. Also, if you are interested in this topic, you may be a good fit for the FullStack Labs family. Why don't you take a look at the opportunities we have available here?

Hugo Rincon
Written by
Hugo Rincon
Hugo Rincon

I first became interested in programming when, at 14, I discovered how to make a simple game using Visual Basic 6. Whether I'm building a game for myself or helping clients change the way their industries operate, the crux of development is always to solve problems and make life better for people. This is why I love Ruby: it makes helping people easy through its intuitiveness, and there's no problem that it can't solve. I've helped car dealers speed up their turnover, construction companies manage projects more easily, and salespeople best take advantage of their stacks of leads rather than get lost in them. I'm meticulous and curious, and when I'm not programming, I enjoy playing videogames and being a dad.

People having a meeting on a glass room.
Join Our Team
We are looking for developers committed to writing the best code and deploying flawless apps in a small team setting.
view careers
Desktop screens shown as slices from a top angle.
Case Studies
It's not only about results, it's also about how we helped our clients get there and achieve their goals.
view case studies
Phone with an app screen on it.
Our Playbook
Our step-by-step process for designing, developing, and maintaining exceptional custom software solutions.
VIEW OUR playbook
FullStack Labs Icon

Let's Talk!

We’d love to learn more about your project.
Engagements start at $75,000.

company name
name
email
phone
Type of project
How did you hear about us?
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.