How Ruby’s method_missing works

by Jason Swett,

What we’re going to cover

In this post we’ll take a look at an example of the kinds of things you can do with method_missing as well as how and why method_missing works.

The example

The following example is a DSL which will allow us to construct an HTML document using Ruby.

(This example, by the way, is shamelessly stolen from a great post by Emmanuel Hayford who apparently himself took the example from the book The Ruby Programming Language. Check out the post and maybe the book too.)

Basic version

Here’s some Ruby code that will generate a rudimentary HTML document.

HTMLDocument.new do
  html do
    body do
      puts "Hello world"
    end
  end
end

The generated HTML code looks like this.

<html>
<body>
Hello world
</body>
</html>

Here’s the code that can get the above DSL code to generate the above HTML output.

class HTMLDocument
  def initialize(&block)
    # This instance_exec means that any message that's
    # sent inside the HTMLDocument.new block (html, body,
    # etc.) will use HTMLDocument as its recipient.
    # See https://www.codewithjason.com/ruby-instance-exec/
    instance_exec(&block)
  end

  private

  def method_missing(method_name, *args, &block)
    puts "<#{method_name}>"
    block.call
    puts "</#{method_name}>"
  end
end

HTMLDocument.new do
  html do
    body do
      puts "Hello world"
    end
  end
end

This works because, thanks to instance_exec, every message that gets sent inside of the HTMLDocument.new do block gets sent to HTMLDocument. Then, because HTMLDocument doesn’t actually respond a message called html or a message called body, method_missing gets invoked for html and body.

And in case it’s not clear, when I say “messages being sent”, I more or less mean “methods being called”. Inside the HTMLDocument.new block, the messages being sent are html, body and puts. The subtle distinction between a message being sent and a method being called is that there’s not always a one-to-one match between messages and methods. There are obviously no methods defined called html or body. Those are just messages that are being sent.

Anyway, inside of out method_missing definition, we say “output an opening tag and a closing tag, and in between those two things, call whatever block that was passed”. The result is the HTML output you see above.

“Advanced” example

Here’s a slightly more “advanced” example. This example will force us to use the *args part of method_missing which we’re not using in the example above.

In this version we use an a tag with an href attribute and a target attribute.

<html>
<body>
Hello world
<a href="https://www.codewithjason.com" target="_top" rel="noopener">
Code with Jason
</a>
</body>
</html>

The idea here is that, for any tag we use, we can optionally specify attributes for the tag by passing arguments. Below is the code that would produce the a tag above.

a(href: "https://www.codewithjason.com", target: "_top") do
  puts "Code with Jason"
end

In order for code like this to work, we need to add something that will take the hash we pass as *args and convert the hash into stringified HTML attributes.

class HTMLDocument
  def initialize(&block)
    instance_exec(&block)
  end

  private

  def method_missing(method_name, *args, &block)
    # args is equal to:
    # [{:href=>"https://www.codewithjason.com", :target=>"_top"}]
    # We're interested in the "first" (and only) element
    puts "<#{method_name}#{hash_to_html_attributes(args[0])}>"
    block.call
    puts "</#{method_name}>"
  end

  def hash_to_html_attributes(hash)
    return unless hash

    stringified_attributes = hash.map do |key, value|
      "#{key}=\"#{value}\""
    end

    " #{stringified_attributes.join(" ")}"
  end
end

HTMLDocument.new do
  html do
    body do
      puts "Hello world"

      a(href: "https://www.codewithjason.com", target: "_top") do
        puts "Code with Jason"
      end
    end
  end
end

Now we’re able to pass arguments to our “missing methods” and HTMLDocument will pick up our arguments.

But why? How does *args work? Let’s go through each of method_missing‘s parameters in detail so we can see.

method_missing, parameter by parameter

We’re going to go through each of method_missing‘s parameters, but out of order. The first parameter, method_name, is a normal one, but the other two are special, each in their own way.

method_name

The method_name argument is simply a stringified version of whatever message was passed. If I do my_object.hello, then method_name will be "hello". Nothing more to it than that.

&block

The &block parameter represents an optional block that we can pass to our method.

You might wonder what the & in front of &block is all about. The explanation is actually pretty long and involved, but the short version is that in order to be able to work with a block, the block first needs to be converted into an instance of the Proc class. The & is what converts the block into a Proc object.

If you’re curious about the details of how this works, see my other post about what the ampersand in front of &block means.

*args

The *args parameter is another slightly wacky one. What’s the * all about?

The * at the beginning of *args is the Ruby **splat operator**. When the splat operator appears in a parameter list, it basically says “send me as many arguments as you want, and I’ll trea

Here’s a method that uses the splat operator.

def list(store, *items)
  "get #{items.join(", ")} from #{store}"
end

puts list("grocery store", "bread", "bananas", "beer")
puts list("hardware store", "nuts", "bolts")

Notice how items gets treated just like an array, even though we didn’t use array syntax to pass the list values.
Here’s the output of the above script.

get bread, bananas, beer from grocery store
get nuts, bolts from hardware store

Takeaways

  • method_missing can be useful for constructing DSLs.
  • method_missing can be added to any object to endow that object with special behavior when the object gets sent a message for which it doesn’t have a method defined.
  • method_missing takes the name of the method that was called, an arbitrary number of arguments, and (optionally) a block.

One thought on “How Ruby’s method_missing works

Leave a Reply

Your email address will not be published.