Take a step back in history with the archives of PragPub magazine. The Pragmatic Programmers hope you’ll find that learning about the past can help you make better decisions for the future.

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

7 min readOct 17, 2023

--

One thing that we learn from exploring the metaprogramming features of Ruby is that nothing is a deep concept.

https://pragprog.com/newsletter/
https://pragprog.com/newsletter/

Talk about an exhausting Monday!

Youve 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 Managera 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.

Youre 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 doesnt take much to find the cause of the error: Bob, your absent-minded colleague, recently lost his cell phone. Now the CONFIGURATION class doesnt return any device for Bobinstead, it returns nil, Rubys 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 shes 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 its 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 dont 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 nothingmight mean returning zero, writing a zero-length file, or returning an empty string. In the case of your Alarm class, doing nothingjust 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 youre 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 its 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 onso 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 objectthe 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 dont 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 dont 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 dont 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 dont.

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 dont need to add the same methods to nil. Instead, nil will simply ignore calls to any method that it doesnt know about.

Beyond Null Objects

Just as youre 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 hes slipped away for good). If wed stuck with the NullDevice class, now wed also need a NullLight class, to call change_to_red and turn_off. Instead, with our nil-based solution, we dont 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 voidmethod. 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. Bills 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!

Cover from PragPub magazine, January 2010

--

--

PragPub
The Pragmatic Programmers

The Pragmatic Programmers bring you archives from PragPub, a magazine on web and mobile development (by editor Michael Swaine, of Dr. Dobb’s Journal fame).