Possibly - Maybe monad for Ruby

Travis CI Code Climate

Maybe monad implementation for Ruby

puts Maybe(User.find_by_id("123")).username.downcase.or_else { "N/A" }

=> # puts downcased username if user "123" can be found, otherwise puts "N/A"

Installation

gem install possibly

Getting started

require 'possibly'

first_name = Maybe(deep_hash)[:account][:profile][:first_name].or_else { "No first name available" }

Documentation

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

The implementation includes three different classes: Maybe, Some and None. Some represents a value, None represents a non-value and Maybe is a constructor, which results either Some, or None.

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

Both Some and None implement four trivial methods: is_some?, is_none?, get and or_else

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

In addition, Some and None implement Enumerable, so all methods available for Enumerable are available for Some and None:

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

All the other methods you call on Some are forwarded to the value.

Maybe("I'm a value").upcase                 => #<Some:0x007ffe198e6128 @value="I'M A VALUE">
Maybe(nil).upcase                           => None

Case expression

Maybe implements threequals method #===, so it can be used in case expressions:

value = Maybe([nil, 1, 2, 3, 4, 5, 6].sample)

case value
when Some
  puts "Got Some: #{value.get}"
when None
  puts "Got None"
end

If the type of Maybe is Some, you can also match the value:

value = Maybe([nil, 0, 1, 2, 3, 4, 5, 6].sample)

case value
when Some(0)
  puts "Got zero"
when Some((1..3))
  puts "Got a low number: #{value.get}"
when Some((4..6))
  puts "Got a high number: #{value.get}"
when None
  puts "Got nothing"
end

For more complicated matching you can use Procs and lambdas. Proc class aliases #=== to the #call method. In practice this means that you can use Procs and lambdas in case expressions. It works also nicely with Maybe:

even = ->(a) { a % 2 == 0 }
odd = ->(a) { a % 2 != 0 }

value = Maybe([nil, 1, 2, 3, 4, 5, 6].sample)

case value
when Some(even)
  puts "Got even value: #{value.get}"
when Some(odd)
  puts "Got odd value: #{value.get}"
when None
  puts "Got None"
end

Examples

Instead of using if-clauses to define whether a value is a nil, you can wrap the value with Maybe() and threat it the same way whether or not it is a nil

Without Maybe():

user = User.find_by_id(user_id)
number_of_friends = if user && user.friends
  user.friends.count
else
  0
end

With Maybe():

number_of_friends = Maybe(User.find_by_id(user_id)).friends.count.or_else { 0 }

Same in HAML view, without Maybe():

- if @user && @user.friends
  = @user.friends.count
- else
  0
= Maybe(@user).friends.count.or_else { 0 }

Tests

rspec spec/spec.rb

License

MIT

Author

Mikko Koski / @rap1ds

Sharetribe / @sharetribe / www.sharetribe.com