Option pattern in Ruby

Have you ever seen this when coding Ruby?

NoMethodError: undefined method `[]' for nil:NilClass

I bet you have, many times—way too many. However, it doesn’t have to be so. There’s a pattern that helps you to get rid errors when handling nil values.

Meet the Option pattern! The idea is simple: wrap the value in a wrapper and treat nil values the same way you would treat non-nil values.

There are many existing gems that use this pattern. I rolled up my own version called Possibly. In this post, all the examples use the Possibly gem.

How does it work?

There are three main concepts in the Option pattern:

  1. Some represent a non-nil value
  2. None represent a nil value
  3. Maybe is a constructor that returns either Some or None
Maybe("I'm a value") 
#=> <Maybe::Some:0x007ff7a85621e0 @value="I'm a value">
Maybe(nil)
#=> <Maybe::None:0x007ff7a852bd20>

Some and None implement four trivial methods: is_some?, is_none?, get and or_else. The first two are obvious: you use them to define whether the value is Some or None.

Maybe("I'm a value").is_some? 
#=> true
Maybe("I'm a value").is_none?
#=> false
Maybe(nil).is_some?
#=> false
Maybe(nil).is_none?
#=> true

The last two are methods meant for unwrapping the Maybe object by returning the value it contains OR the default value if the object is None.

Maybe("I'm a value").get 
#=> "I'm a value"
Maybe("I'm a value").or_else { "No value" }
#=> "I'm a value"
Maybe(nil).get
#=> RuntimeError: No such element
Maybe(nil).or_else { "No value" }
#=> "No value"

Notice the difference between get and or_else. If you call get for None it throws an error. Thus it’s always recommended to use or_else.

Enumerable interface

Maybe implements all the methods of the Enumerable interface:

Maybe("Print me!").each { |v| puts v } 
#=> it puts "Print me!"
Maybe(nil).each { |v| puts v }
#=> puts nothing
Maybe(4).map { |v| Math.sqrt(v) }
#=> <Maybe::Some:0x007ff7ac8697b8 @value=2.0>
Maybe(nil).map { |v| Math.sqrt(v) }
#=> <Maybe::None:0x007ff7ac809b10>
Maybe(2).inject(3) { |a, b| a + b }
#=> 5
Maybe(nil).inject(3) { |a, b| a + b }
#=> 3

Please note that Maybe does not implement Array methods, only Enumerable methods.

The Enumerable methods provide the real power behind the Maybe pattern. I’ll give an example to demonstrate this.

Consider the following situation: you have a Rails app and a variable @current_user is set if the user has logged in. The @current_user has one profile which contains the user’s real name. Your task is to print the real name or “Real name unknown”.

Typically in HAML, you would do this:

- if @current_user && @current_user.profile && @current_user.profile.real_name.present? 
= @current_user.profile.real_name
- else
= "Real name unknown"
- end

With Maybe, you can simplify the code to:

= Maybe(@current_user)
.map { |user| user.profile }
.map { |profile| profile.real_name }
.or_else { "Real name unknown" }

The real benefit of the latter is that you are treating nil values the same way as non-nil values. In the former version, you may easily forget the if-clause and just use @current_user.username, which results in an error if @current_user is nil. If you use the latter, you don’t need to worry about the if-clause.

In addition, the latter version is DRYer. In the former version, @current_user is mentioned 4 times, profile 3 times and real_name 2 times. If you ever decide to rename any of these, you’ll need to change them all.

However, you may notice that calling the map method twice is not that DRY.

Map shorthand

Mapping the value wrapped in a Maybe object is something you end up doing a lot. Let’s continue with the previous example with one addition: the real name needs to be lowercased.

= Maybe(@current_user)
.map { |user| user.profile }
.map { |profile| profile.real_name }
.map { |real_name| real_name.downcase }
.or_else { "Real name unknown" }

That’s a lot of typing! There’s a neat shorthand way to clean up this long line of code: a call to a method that is not defined in Maybe (i.e. is not is_some?, is_none?, get, or_else nor any Enumerable method) is treated as a map call. In other words, the previous example is equivalent to:

= Maybe(@current_user).profile.real_name.downcase.or_else { "Not logged in" }

Rails example — Handle params hash in controller

Let’s consider a situation where you need to pull a user’s address and city from a params hash, geocode it as a latitude/longitude pair, and update the model if the address parameter exists and the geocoder returns a non-nil value (i.e. the geocoding was successful):

def update_latlng 
latlng = Maybe(params)[:account][:profile][:location].map do |location|
my_geocoder(location[:address], location[:city])
end
  latlng.each do |latlng_value| 
@current_user.latlng = latlng_value
@current_user.save
end
end
private 
def my_geocoder(street_address, city) 
# Geocode by the street address and a city
# Return string representation of lat/lng e.g "12.345,23.456"
# or nil if unable to geocode
end

Let’s go through the example step by step.

First, the params hash is wrapped with Maybe and traversed to the location hash. If params[:account][:profile][:location] exists, then Maybe(params)[:account][:profile][:location] returns Some with the location as a value. Otherwise it returns None.

Second, the location hash is mapped to latitude/longitude by passing relevant location info to the my_geocoder method. The map method returns a new Maybe object which is either Some if the geocoding was successful or None if my_geocoder returned nil.

Finally, the latitude/longitude value of the current user is updated and saved. If the geocode mapping returns Some, then the each block is called once. If it returns None, then the each block is not called.

Wrapping up

The Option pattern is not a new invention. It is widely used in many functional languages, such as Haskell, SML and Scala. Not all patterns that work well in functional languages work in Ruby, an object-oriented language. However, but in my opinion, the Option pattern is a perfect fit for Ruby and Rails developers.

Since there are many existing Ruby implementations of the Option pattern, it is fair to ask, why do we need yet another one?

The existing implementations seem to miss the fact that, in essence, Maybe can be considered an Array. None is an empty array ([]) and Some is an array with one element ([“I’m the value”]). Also, a couple of weeks ago, Ruby Weekly included an article by Brian Cardarella, Guarding with Arrays, which is essentially the same thing as what the Possibly gem does internally. With this in mind, it feels natural that Maybe implements the Enumerable interface. The Enumerable interface provides powerful tools to use and alter the value of Maybe. It’s also a familiar interface to all Ruby developers since they’ve been using plain old arrays a long time.

We at Sharetribe have been happily using the Possibly gem for a couple of months, and we’re already seeing how it cleans up our code little by little. Feel free to try it out, report an issue if you find one or send a Pull Request on Github!

Update: Previously, the article stated the Possibly gem is a Ruby implementation of Haskell’s Maybe Monad, which was incorrect. This statement is now removed. Thanks to active commentators on Reddit!

- Mikko


Originally published on the Sharetribe blog on June 2, 2014.