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