INDIA +91 964 309 2571 | USA +1669 327 4700 info@navyuginfo.com

Let’s start with a problem every Ruby on Rails developer faces, handling ‘nil’ value – a tedious task.

NoMethodError: undefined method `[]' for nil:NilClassy 4 spaces

But there is a well-known design pattern to handle ‘nil’ value for Ruby in the more robust way. Like pure functional programming languages does, with the ‘Monad’ design pattern.

Monad is a design pattern which is used to describe expressions as a series of actions. Monad generally, wraps the datatype with some extra information. And a well-known monad for handling ‘nil’ value is called ‘Maybe’ monad.

Maybe monad is a programming pattern that allows treating nil values in the same way as non-nil values. This is done by wrapping the value, which may or may not be nil to, a wrapper class.

There is already a gem called ‘possibly’ that handles ‘nil’ values as a special data type, ‘possibly’ is the implementation of Haskell’s Maybe Monad.

Working

Maybe("I'm a value")
#<Some:0x007ff7a85621e0 @value="I'm a value">

Some – represent a non-nil value.
None – represent a nil value.

Maybe(nil)
=> #<None:0x0000000c83a020>

Maybe is type constructor.

 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
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                              => None::ValueExpectedException: `get` called to None. A value was expected.
Maybe(nil).or_else { "No value" }           => "No value"
Maybe("I'm a value").or_raise               => "I'm a value"
Maybe(nil).or_raise                         => None::ValueExpectedException: `or_raise` called to None. A value was expected.
Maybe(nil).or_raise(ArgumentError)          => ArgumentError
Maybe("I'm a value").or_nil                 => "I'm a value"
Maybe([]).or_nil                            => nil

example methods to extract values out of the “Maybe” object. like

Maybe("I'm a value").is_some?
=> true

Handling Enumerable through “Maybe” Monad

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) }           => #<Some:0x007ff7ac8697b8 @value=2.0>
Maybe(nil).map { |v| Math.sqrt(v) }         => #<None:0x007ff7ac809b10>
Maybe(2).inject(3) { |a, b| a + b }         => 5
None().inject(3) { |a, b| a + b }           => 3

consider an example:

 Maybe(nil).map { |v| Math.sqrt(v) } 
=> <Maybe::None:0x007ff7ac809b10>

more real-world use case of latter example would be:

Maybe(nil).map { |v| Math.sqrt(v) } .or_else {'Sorry'}
=> "Sorry"

Assume in your Rails app, @current_user variable is set when the user is logged in. The @current_user has one account which contains the user’s name. Your task is to print the name or “ name is not defined”.

In HAML,

- if @current_user && @current_user.account && @current_user.account.name.present? 
  = @current_user.account.name 
- else 
  = "name is not defined" 
- end

we can simplify this code, with the help of the ‘Maybe’.

= Maybe(@current_user)
  .map { |user| user.account } 
  .map { |account| account.name } 
  .or_else { "name is not defined" }

Next example follows the DRY principle, fewer lines of code, more readable.
One catch in working with the “Maybe”, is that you have to write ‘map’ calls so many times, there exists a different version of the latter code, without map calls.

= Maybe(@current_user).account.name.or_else { "Not logged in" }

tip: ‘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’

Some more use cases in Rails

Imagine you have the following params hash in the controller action

   (byebug) params
 
   {transaction: {id: 1, booking: {id:1, default_address: 1, renter_shipping_info: {id: 1, delivery_timeslot_id:nil}
            }}}

and you have to update the renter’s shipping info (delivery timeslot ), if delivery timeslot id exists.

def renter_shipping_info_update
 renter_timeslots = Maybe(params[:transaction][:booking]    [:renter_shipping_info][:delivery_timeslot_id]).map do |timeslot_id|
        
                Timeslot.find(timeslot_id)
            end
  
 @current_user.delivery_timeslot = renter_timeslots
 @current_user.update!
end

Explanation:

params is wrapper with Maybe and traversed till
‘:delivery_timeslot_id’.
If params[:transaction][:booking] [:renter_shipping_info][:delivery_timeslot_id] exists, then Maybe(params)[:transaction][:booking][:renter_shipping_info][:delivery_timeslot_id] returns ‘Some’ with the id as a value. Otherwise it returns None.

By – Siddartha Jangid, Software Engineer