Skip to content

asthasr/ruby-indubitably

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚠️ This library is no longer maintained, and I do not recommend using it or any similar library in Ruby.

Indubitably - Maybe monad for Ruby.

This is a Maybe implementation for Ruby. It is production-ready and battle-tested!

Travis CI

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 indubitably

Getting started

require 'indubitably'

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("I'm a value").or_nil                 => "I'm a value"
Maybe(nil).get                              => RuntimeError: No such element
Maybe(nil).or_else { "No value" }           => "No value"
Maybe(nil).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

Value Injection (New)

You can use the #if_some method to inject a value into a Some:

Some(7).if_some(:foo)                       => Some(:foo)
Some(7).if_some { 'argyle socks' }          => Some('argyle socks')
None().if_some(:foo)                        => None

This is primarily useful when you are making a decision for an unrelated value, with value.if_some(:foo).or_else(:bar) replacing value.is_some? ? :foo : :bar.

Joining

In 0.3.0, there is new functionality to make it easy to "flatten" a nested Maybe structure:

Maybe(Some(7)).join                         => Some(7)
Maybe(Maybe(Some(7))).join!                 => Some(7)
Maybe(None()).join                          => None()
Maybe(Maybe(None())).join!                  => None()

This is equivalent to join from the Control.Monad package in Haskell, or x >>= id. There is also an option to join statically, so that you can wrap values that may already be Maybe:

Maybe.join?(7)                              => Some(7)
Maybe.join?(Some(3))                        => Some(3)
Maybe.join?(None())                         => None()

Forcing Value Dispatch

Unfortunately, some of the method names for Enumerable (and thus Maybe) clash with methods that you might want to call on the wrapped value. If you use an underscore at the beginning of the method name, it will be dispatched to the wrapped value:

Some([2, 3, 4])._map { |n| n * n }          => Some([4, 9, 16])

This also has the side-effect of making it easier to call methods on nested structures without flattening them completely:

Some(Some([2, 3, 4])).__map { |n| n * n }    => Some(Some([4, 9, 16]))

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

Authors

About

A maybe monad

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%