FROM THE ARCHIVES OF PRAGPUB MAGAZINE JANUARY 2010
Much Ado About Nothing: Gazing into the Abyss of Ruby’s nil, Amongst Null Objects, Ghost Methods, and Black Holes
By Paolo Perrotta
One thing that we learn from exploring the metaprogramming features of Ruby is that nothing is a deep concept.
Talk about an exhausting Monday!
You’ve spent the entire day pair programming with Bill, your excellent, experienced, and sometimes exasperating buddy. The two of you are putting the final touches to the Alarm Manager — a useful piece of Ruby code that sends alerts to your colleagues when the main company-wide application has a glitch.
The Alarm Manager is just a short program, but it sports a flexibility to rival Twitter. It can send different kinds of alarms to any remote device, from brand new iPhones down to vintage pagers. Here is the cornerstone Alarm Manager
class:
class Alarm
def device
CONFIGURATION.current_user.device
end
def send_default
10.times { device.ring }
end
def send_discreet device.ring
end
def send_silent
100.times { device.flash }
end
end
The Alarm#device
method reads the device associated with the current user from a CONFIGURATION
constant. Your program can send alarms to any device, as long as the device can flash a light and ring. The send_default
method rings ten times, send_discreet
rings only once, and send_silent
just flashes the light for a while.
You’re just one click away from a working Alarm Manager. After configuring the system with the devices of all your colleagues, you and Bill fire the last functional test . . . and watch in horror as it fails.
NoMethodError: undefined method ‘ring’ for nil:NilClass
Luckily, it doesn’t take much to find the cause of the error: Bob, your absent-minded colleague, recently lost his cell phone. Now the CONFIGURATION
class doesn’t return any device for Bob — instead, it returns nil
, Ruby’s value for an uninitialized object reference. When Alarm
attempts to call ring
or flash
on nil
, the call fails with a NoMethodError
.
Uninitialized references are a common nemesis of object-oriented programmers. (Just ask a Java coder how many NullPointerExceptions
she’s seen in her life!) How can you avoid this problem?
As a first attempt, you and Bill check the object returned by device to ensure that it’s not nil
. For example, Alarm#send_default
becomes:
class Alarm
def send_default
return unless device
10.times { device.ring }
end
# ...
It just takes that single line of code to push old Bill into one of his legendary bursts of complaining. “I don’t want to add an extra if
to each and every method!” he exclaims. “I think I know a better way to fix this problem. Let me show you something . . . ”
Null Objects
Bill is eager to show you a magic programming spell known as Null Object. A Null Object is just a regular object whose methods do nothing.
Bill grabs a scrap of paper and draws a picture to show you how a Null Object is supposed to work. The idea is that you can replace an uninitialized reference with a Null Object. You can then call methods on the object, and the object will trash your messages without raising an error.
In some situations, “doing nothing” might mean returning zero
, writing a zero-length file, or returning an empty string. In the case of your Alarm
class, “doing nothing” just means that both the flash
and ring
methods are empty:
class NullDevice
def flash; end
def ring; end
end
Alarm#device
can now return the configured device if it exists, and a NullDevice
if it doesn’t:
class Alarm
def device
CONFIGURATION.current_user.device || NullDevice.new
end
# ...
If you’re new to Ruby, you might find the line of code in device
confusing. Bill explains that this idiom is called a Nil Guard. Alarm#device
returns CONFIGURATION.current_user.device
only if it’s not nil
.
If CONFIGURATION.current_user.device
is nil
(or false
), then Alarm#device
returns a NullDevice
. Now, no matter what happens, the device method will always return something that you can safely call ring
and flash
on — so you can avoid littering the callers of Alarm#device
with defensive ifs
.
However, grumpy old Bill is not satisfied yet. “This Null Object looks really Java-ish,” he mumbles, frowning. “I think that we can make it more like idiomatic Ruby. Here is how…”
Playing Ruby’s Strengths
In most languages, an uninitialized object reference is just a big arrow pointing at nothing. On the other hand, in Ruby there is no such thing as “nothing.” The nil
value is actually a regular object — the sole instance of NilClass
:
nil.class # => NilClass
Another powerful feature of Ruby is that classes are never closed. You can always re-open and modify an existing class, NilClass
included. This means that there is no need for a NullDevice
class. Instead, you can define flash
and ring
on NilClass
itself:
class NilClass
def flash; end
def ring; end
end
Now you don’t need to check whether CONFIGURATION.current_user.device
is nil
. In fact, nil
itself has become a Null Object
: you can call flash
and ring
on it, and it will do nothing. Wonderful!
However, you probably don’t want to pollute NilClass
with your own domain-specific methods. As Bill is ready to observe, you can find a more elegant solution in yet another Ruby magic spell: a special method named method_missing
. If you implement method_missing
, it will intercept all calls to methods that don’t exist. Just replace NilClass#flash
and NilClass#ring
with method_missing
, and method_missing
will take care of all calls to nil
:
class NilClass
# Calls to flash(), ring() or other unknown methods
# end in method_missing().
# The asterisk means that arguments are ignored.
def method_missing(*); end
end
Now you can say that flash
and ring
are Ghost Methods, because the caller thinks they exist, but actually they don’t.
Bill draws a picture to show you how your new Null Object
works:
If you use Ghost Methods instead of flesh-and-bones methods like flash
and ring
, you also get another advantage for free: if you add new methods to your devices (say, a beep
method), then you don’t need to add the same methods to nil
. Instead, nil
will simply ignore calls to any method that it doesn’t know about.
Beyond Null Objects
Just as you’re about to turn off your computer and call it a day, you and Bill get one of those last-minute change requests. Your boss wants an Alarm#send_urgent
method for devices that have a controllable light:
class Alarm
def send_urgent
100.times do
device.light.change_to_red
device.ring
device.light.turn_off
end
end
# …
“This is wonderful!” Bill exclaims, smiling for the first time this week. (For a moment, you think that he’s slipped away for good). “If we’d stuck with the NullDevice
class, now we’d also need a NullLight
class, to call change_to_red
and turn_off
. Instead, with our nil-based solution, we don’t have to write a single line of code. Here, let me draw a picture…”
The trick here is that Ruby has no such thing as a “void
” method. Instead, each and every method always returns a value. In particular, an empty method always returns nil
. Now, look at your NilClass
again:
class NilClass
def method_missing(*); end
end
NilClass#method_missing
is empty, so it returns nil
. This means that any call to a Ghost Method on nil
returns nil
itself. You can then call another Ghost Method on nil
, chaining arbitrary calls like in the case of device.light.change_to_red
. When used this way, a Null Object such as nil
is also called a Black Hole. A Black Hole sucks your method calls into an infinite depth of Null Objects, avoiding the problems associated with uninitialized references.
On the other hand, Black Holes can cause their own share of trouble. After all, NoMethodErrors
are there for a reason: they help you spot bugs in your code. A Black Hole can mask those bugs and make it more difficult for you to notice when things go wrong. Bill’s recommendation is that you should try Black Holes yourself, and decide whether they work for you. After all, there is at least one popular language (Objective C) where all null references are Black Holes by default.
However, this interesting discussion can wait for another day. Today, you and Bill can finally slip out of the office and dump yourselves in the nearest pub!
About the Author
Meet the author of this article and his books, Programming Machine Learning and Metaprogramming Ruby 2, from The Pragmatic Bookshelf.
Learn more about Paolo and his journey from programmer to author in his author spotlight on Devtalk: