How to Implement Metaprogramming in Ruby with define_method
In this guide let's learn about another metaprogramming mechanism called define _method that will allow you to dynamically create methods at runtime in a Ruby program.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

In this guide let's learn about another metaprogramming mechanism called define _method that will allow you to dynamically create methods at runtime in a Ruby program.

We'll continue with our Author class, and use a method called define_method here.

class Author
  define_method("some_method") do
    puts "Some details"
  end
end

Next, we'll call this method.

author = Author.new
author.some_method

When you run this code, it'll print out the value "Some details".

Now, you may wonder how this is different from a regular method such as this one:

def some_method
  puts "Some details"
end

The answer is: they are the same!

So, this is not the kind of implementation you'd use define_method for in the real world. However I wanted to start with this as a base case so you can see the core functionality.

Now, let's look at a more practical example. Going back to our Author class, let's imagine that we have different genres and we have to print out the details for each of the different genres. To do that, let's create a few methods inside our Author class, like this.

class Author
  def fiction_details(arg)
    puts "fiction"
    puts arg
    puts "something else..."
  end

  def coding_details(arg)
    puts "coding"
    puts arg
    puts "something else..."
  end

  def history_details(arg)
    puts "history"
    puts arg
    puts "something else..."
  end
end

Each of these methods simply print out the values based on the arguments that are passed to it. Let's call one of the methods now with the code,

author = Author.new
author.coding_details("Cal Newport")

When you run this code, it will print out:

coding
Cal Newport
something else...

Though this implementation works, it's showcasing some poor programming practices because of the amount of identical code we are using. This is where define_method is useful. A better implementation would be to utilize define_method, which would look something like this:

class Author
  genres = %w(fiction coding history)

  genres.each do |genre|
    define_method("#{genre}_details") do |arg|
      puts "Genre: #{genre}"
      puts arg
      puts genre.object_id
    end
  end
end

author = Author.new
author.coding_details "Cal Newport"

In this code, we are creating a variable called genres. If you're not familiar with %w(...) syntax, it is a way that Ruby allows us to create an array of strings without commas and double quotes. In the next line, we are iterating over the genres array, and inside of the each block we are calling define_method. Inside of this block we are printing the name of the genre and the value of arg. Lastly, we are generating a unique ID with the code genre.object_id to show that the object values will be unique.

Essentially what this code is doing is it iterates over our genres array, and based on the genre, it will dynamically create a method that will have the name of the genre followed by the word _details. If you notice, the functionality is the same as before, but this time we have to define it only once, and obviously, the amount of code is greatly reduced.

Now if you run the code, the output will be:

Genre: coding
Cal Newport
70338847194560

Let's add another line:

author.fiction_details("Ayn Rand")

And now the result is:

Genre: coding
Cal Newport
70107179007240
Genre: fiction
Ayn Rand
70107179007260

So, that's all working fine. See how easy and compact your code is when you use define_method?

There's also another cool thing about this method. If you remember our method_missing guide and how we had to integrate our 'respond_to?` method to get it working properly? You can call that method here like this.

p author.respond_to?("coding_details")

When you execute it, the program will output true.

In the method_missing guide we had to build another method simply to ensure that the respond_to? method will work properly (which is key to us following best practices for metaprogramming). However, when we use define_method, it's actually creating methods for us at runtime, and the respond_to method comes by default.

At this point, I'd like to point out a subtle difference that exists between the methods define_method and method_missing in Ruby. Since define_method creates the methods for us at runtime, we get the functionality of respond_to method for free. On the other hand, the method_missing method does not trigger a call until the method call has gone through the method call cycle with Ruby. For example, let's say we had a call for a method named summary like this:

author.summary

Ruby will go to Author class and look for a method called summary. When it can't find it there, it'll go up the chain and look for this method in BasicObject and all the other built-in classes that Ruby has to see if there is a summary method somewhere that it can call. When Ruby can't find the method anywhere, it comes back to the Author class and checks to see if there is a method_missing definition, and if there is there a summary method in it. This is why we had to add the respond_to method earlier in order to get it working. With define_method, we don't have to write any code for respond_to as it works right out of the box for us.

However, the downside with define_method is its limited flexibility. With method_missing, you can do pretty much anything when there is a method call for a non-existent method, but with define_method, you can't do anything other than defining methods from a pre-defined list of values. Also, we have to provide all the necessary details at the time of writing the code to ensure that define_method works properly. However with missing_method, you can decide what you want to do when the call comes in at runtime.

In short, you can use define_method when you know what needs to be done. In fact, it's key use is to create methods for you at runtime based on the details you provide in the code - just like how we did. In this sense, it's a great way to DRY up your code and to have a nice code structure.

Resources