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.
Thanks Jason!