Option pattern in Ruby

Have you seen this?

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 of the errors you get when handling nil-values.

Meet 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. Also, 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 Option pattern:

- Some represent a non-nil value
None represent a nil-value
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 can use them to defining 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 Enumerable module has:

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.

Enumerable methods provide the real power behind the Maybe pattern. Let’s take an example to demonstrate this.

Consider the following situation: You have a Rails app and a variable @current_user is set if user has logged in. The @current_user has one profile which contains 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 value the same way as non-nil value. In the former version you may easily forget that if-clause and just type @current_user.username, which results 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 to clean up this long line of code: Call to a method that is not defined in Maybe (i.e. neither is_some?, is_none?, get, or_else nor any Enumerable method) are treated as a map call. So, 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 users address and city from a deep params hash and geocode it to latitude/longitude pair and update the model if 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 = latlgn_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 steps.

First, the params hash is wrapper to 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 my_geocoder method. The map method returns a new Maybe object which is either Some if geocoding was successful or None if my_geocoder returned nil.

Last, 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

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 nicely in functional languages work in Ruby (OO-language), but in my opinion, Option pattern is a perfect fit for Ruby and Rails developers.

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

The existing implementations seem to miss the fact that in essence Maybe can be considered as an Array. None is an empty array ([]) and Some is an array with one element (["I'm the value"]). Also, couple weeks ago Ruby Weekly included an article by Brian Cardarella, Guarding with Arrays, which is essentially the same thing Possibly gem does internally. With this in mind, it feels natural that Maybe implements the Enumerable interface. The Enumerable provide powerful tools to use and alter the value of Maybe. Also, the method names are familiar 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 months and we’re already seeing how it cleans up our code little by little. Feel free to try it out and 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!

3 thoughts on “Option pattern in Ruby

  1. Pingback: A Swift Outlook | Futurice blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s