Okay, that’s enough priming of your brain. Let’s do some refactoring.

Handling nil

First, I’d like to look at some code that has to deal with nil s.

Imagine that we have a project management application with different kinds of models:

Project = Struct.new(:creator) Person = Struct.new(:address) Address = Struct.new(:country) Country = Struct.new(:capital) City = Struct.new(:weather)

Each Project has a Person who created it; each Person has an Address ; each Address has a Country ; each Country has a capital City ; and each City has weather information, which, for the sake of simplicity, is just a string.

Let’s say that we want to display the weather next to each project in our user interface (for some reason). That involves traversing all these associations. Here’s a method that does that:

def weather_for(project) project.creator.address. country.capital.weather end

(Maybe you’ve written similar Rails view helpers before. There are lots of reasons not to write code like this, but there are also perfectly good reasons to do it, and anyway, people will always write code like this no matter what we say.)

If we make a city which has sunny weather, and a country which has that city as its capital, and an address in that country, and a person with that address, and a project created by that person…

>> city = City.new('sunny') => #<struct City …> >> country = Country.new(city) => #<struct Country …> >> address = Address.new(country) => #<struct Address …> >> person = Person.new(address) => #<struct Person …> >> project = Project.new(person) => #<struct Project …>

…then we can pass that project into #weather_for and it works fine:

>> weather_for(project) => "sunny"

But if we make a bad project, for example by providing an address that has no country, #weather_for blows up:

>> bad_project = Project.new(Person.new(Address.new(nil))) => #<struct Project …> >> weather_for(bad_project) NoMethodError: undefined method `capital' for nil:NilClass

Tony Hoare invented nil in 1965; he now calls it his “billion-dollar mistake”, which has “probably caused a billion dollars of pain and damage”. This is exactly the sort of thing he’s talking about.

Well, they may be a mistake, but Ruby has nil s, so we’re stuck with them. To make #weather_for tolerate nil s, we’re going to have to explicitly check for them.

First we need to introduce local variables to hold every intermediate result…

def weather_for(project) creator = project.creator address = creator.address country = address.country capital = country.capital weather = capital.weather end

…and then check each intermediate result before we try to call a method on it:

def weather_for(project) unless project.nil? creator = project.creator unless creator.nil? address = creator.address unless address.nil? country = address.country unless country.nil? capital = country.capital unless capital.nil? weather = capital.weather end end end end end end

(While we’re at it, we might as well include the possibility that the project itself is nil .)

The method body is starting to drift right and become a pyramid of doom, but luckily it works the same if we flatten it:

def weather_for(project) unless project.nil? creator = project.creator end unless creator.nil? address = creator.address end unless address.nil? country = address.country end unless country.nil? capital = country.capital end unless capital.nil? weather = capital.weather end end

This code works, but it’s pretty clumsy, and it’s hard to remember to do something like this every time we might possibly have nil s to deal with.

Fortunately, Ruby on Rails has a solution to this problem. Rails (actually Active Support) monkey patches Object and NilClass with a method called #try , which delegates to #public_send if the object’s not nil , and returns nil otherwise:

class Object def try(*a, &b) if a.empty? && block_given? yield self else public_send(*a, &b) if respond_to?(a.first) end end end class NilClass def try(*args) nil end end

When every object has a #try method, instead of doing these nil checks ourselves, we can let #try do it for us:

def weather_for(project) creator = project.try(:creator) address = creator.try(:address) country = address.try(:country) capital = country.try(:capital) weather = capital.try(:weather) end

Now we’re back to just chaining method calls together, so we can take the local variables out again:

def weather_for(project) project. try(:creator). try(:address). try(:country). try(:capital). try(:weather) end

This is good as it gets right now — better than the version with “ unless nil? ” all over the place, at least. Can we do any better?

Well, monkey patching has its place, but monkey patching every single object in the system isn’t great. It’s kind of a code smell, so let’s not do it.

When we want to add a method to an object, the good object-oriented programming solution is to use decoration, where we non-invasively add functionality to one object by wrapping it up inside another object.

Let’s make a decorator class, Optional , whose instances have a single attribute called value :

Optional = Struct.new(:value)

Instances of this class just wrap up another value. We can wrap a value like 'hello' , then take 'hello' out again later:

>> optional_string = Optional.new('hello') => #<struct Optional value="hello"> >> optional_string.value => "hello"

If the value we put in happens to be nil , we get nil out later:

>> optional_string = Optional.new(nil) => #<struct Optional value=nil> >> optional_string.value => nil

So now, instead of putting the #try method on Object , let’s put it on Optional :

class Optional def try(*args, &block) if value.nil? nil else value.public_send(*args, &block) end end end

If the value attribute is nil , #try just returns nil , otherwise it sends the appropriate message to the underlying object.

Now we can call #try on the decorator and it’ll call the method on the underlying object as long as it’s not nil :

>> optional_string = Optional.new('hello') => #<struct Optional value="hello"> >> length = optional_string.try(:length) => 5

If the value inside the Optional is nil , #try will just return nil :

>> optional_string = Optional.new(nil) => #<struct Optional value=nil> >> length = optional_string.try(:length) => nil

So instead of calling #try on the actual project object, and then on the actual person object, and so on…

def weather_for(project) creator = project.try(:creator) address = creator.try(:address) country = address.try(:country) capital = country.try(:capital) weather = capital.try(:weather) end

…we can write the method like this:

def weather_for(project) optional_project = Optional.new(project) optional_creator = Optional.new(optional_project.try(:creator)) optional_address = Optional.new(optional_creator.try(:address)) optional_country = Optional.new(optional_address.try(:country)) optional_capital = Optional.new(optional_country.try(:capital)) optional_weather = Optional.new(optional_capital.try(:weather)) weather = optional_weather.value end

First we decorate project with an Optional object, and call #try on that. Then we decorate the result, which might be nil , and call #try again. Then we decorate the next result, and call #try on that, and so on. At the end, we pull out the value and return it.

It’s unwieldy, but hey, at least we’re not monkey patching every object in the system.

There’s another code smell here: #try does too much. We actually just wanted to refactor away the nil check, but #try also sends the value a message. What if we want to use the value in some other way when it’s not nil ? Our #try method is overspecialised; it has too much responsibility.

Instead of hard-coding the else clause inside #try , let’s allow its caller to supply a block that controls what happens next:

class Optional def try( &block ) if value.nil? nil else block.call(value) end end end

Now we can pass a block to #try , and do whatever we want with the underlying value: send it a message, or use it as an argument in a method call, or print it out, or whatever. (This ability to supply a block is actually a little-used feature of Active Support’s #try too.)

So now, instead of calling #try with a message name, and having to remember that it’s going to send that message to the underlying object, we call it with a block, and in the block we send the message ourselves and decorate the result in an Optional :

def weather_for(project) optional_project = Optional.new(project) optional_creator = optional_project.try { |project| Optional.new(project.creator) } optional_address = optional_creator.try { |creator| Optional.new(creator.address) } optional_country = optional_address.try { |address| Optional.new(address.country) } optional_capital = optional_country.try { |country| Optional.new(country.capital) } optional_weather = optional_capital.try { |capital| Optional.new(capital.weather) } weather = optional_weather.value end

And we’re now able to do whatever we want with the value, like print it out in a log message.

That works fine when there aren’t any nil s, but unfortunately we’ve broken it when nil s are involved, because we’re returning nil when the block doesn’t run:

>> weather_for(project) => "sunny" >> weather_for(bad_project) NoMethodError: undefined method `capital' for nil:NilClass

That’s easy to fix. Instead of returning a raw nil , we’ll decorate it with an Optional first:

class Optional def try(&block) if value.nil? Optional.new(nil) else block.call(value) end end end

And now it works in both cases:

>> weather_for(project) => "sunny" >> weather_for(bad_project) => nil

But there’s a new code smell: I don’t think #try is a great name any more, because we’ve changed it to do something more general than, or at least something different from, the main use case of its Active Support namesake.

Let‘s rename it to #and_then :

class Optional def and_then (&block) if value.nil? Optional.new(nil) else block.call(value) end end end

Because it really just says, “start with this decorated value, and then do some arbitrary thing with it, as long as it’s not nil ”.

Here’s the new version of #weather_for , which calls #and_then instead of #try :

def weather_for(project) optional_project = Optional.new(project) optional_creator = optional_project. and_then { |project| Optional.new(project.creator) } optional_address = optional_creator. and_then { |creator| Optional.new(creator.address) } optional_country = optional_address. and_then { |address| Optional.new(address.country) } optional_capital = optional_country. and_then { |country| Optional.new(country.capital) } optional_weather = optional_capital. and_then { |capital| Optional.new(capital.weather) } weather = optional_weather.value end

And because we’re just chaining #and_then calls, we can get rid of the local variables again:

def weather_for(project) Optional.new(project). and_then { |project| Optional.new(project.creator) }. and_then { |creator| Optional.new(creator.address) }. and_then { |address| Optional.new(address.country) }. and_then { |country| Optional.new(country.capital) }. and_then { |capital| Optional.new(capital.weather) }. value end

This is verbose but nice: we decorate the (possibly nil ) project in an Optional object, then safely traverse all the associations, then pull the (possibly nil ) value out again at the end.

Phew, okay. How’s our refactoring going?

Well, we might not be monkey patching anything, and it’s conceptually clean, but there’s a huge final smell: nobody wants to write code like this! In theory it might be better than Active Support’s #try method, but in practice it’s worse.

But we can add some syntactic sugar to fix that. Here’s a definition of #method_missing for Optional :

class Optional def method_missing(*args, &block) and_then do |value| Optional.new(value.public_send(*args, &block)) end end end

It uses #and_then to delegate any message to the underlying value whenever it’s not nil . Now we can replace all of the “ and_then … Optional.new ” stuff with just normal message sends, and let #method_missing take care of the details:

def weather_for(project) Optional.new(project). creator.address.country.capital.weather. value end

This is actually really good! You can see very clearly that we wrap up the possibly- nil project into an Optional , then safely perform our chain of method calls, then pull the possibly- nil weather out of an Optional at the end.

To recap, here’s the full definition of Optional :

Optional = Struct.new(:value) do def and_then(&block) if value.nil? Optional.new(nil) else block.call(value) end end def method_missing(*args, &block) and_then do |value| Optional.new(value.public_send(*args, &block)) end end end

We designed an object which stores a value that might be nil , and a method called #and_then which encapsulates the nil -check logic. We added some sugar on top by writing #method_missing . (If this was production code, we should remember to implement #respond_to? as well.)

I’d like to very briefly point out that we only need to do the decorating and undecorating for compatibility with the rest of the system. If the rest of the system passed in an Optional and expected us to return one, we wouldn’t even need to do that:

def weather_for(project) project.creator.address. country.capital.weather end