Monads implemented in Ruby with sugar. Inspired by @tomstuart's talk https://codon.com/refactoring-ruby-with-monads
Add this line to your application's Gemfile:
gem 'ronad', require: 'ronad/sugar'
Alernatively without sugar
gem 'ronad'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ronad
If required, you can use the monads as methods:
Maybe('hello')
Just('world')
Default('hello', 'world')
Without the sugar you must use the fully qualified name and invoke the constructor:
Ronad::Maybe.new('hello')
Ronad::Just.new('world')
Ronad::Default.new('hello', 'world')
All examples will be using the "sugar" syntax but they are interchangeable.
Generally every Ronad::Monad
will respond to and_then
.
method_missing
is also used to for convenience as a proxy for and_then
.
maybe_name = Maybe({person: {name: 'Bob'}})
maybe_name
.and_then{ |v| v[:person] }
.and_then{ |v| v[:name] }
.value
# Equivalent:
maybe_name = Maybe({person: {name: 'Bob'}})
~maybe_name[:person][:name]
Generally the and_then
block will only run based on a condition from the specific monad.
For example, Maybe
will only invoke the block from and_then
if the underlying value is not
nil
.
Maybe(nil).and_then do |_|
raise "Boom"
end #=> No error
To get the underlying value of a monad you need to call #value
. If the underlying value also
responds to #value
, you can use #monad_value
. #~
is a unary operator overload which is an
alias for monad_value
.
m = Maybe(5)
m.value == m.monad_value #=> true
m.value == ~m #=> true
m.monad_value == ~m #=> true
Maybe
is useful as a safe navigation operator:
response = request(params) #=> {}
name = ~Maybe(response[:person])[:name] #=> nil
It's also useful as an annotation. If you look at the previous example, you'll notice that you could
wrap response
in a maybe instead:
name = ~Maybe(response)[:person][:name] #=> nil
This functionally the same but has different semantics. With the former example, it has correctly
annotated that response[:person]
can be nil
, whereas the latter example is an over eager usage.
Here's a less trivial example
Document.each do |doc|
maybe_json = Maybe(doc.data) # data is nil or a json string
.and_then{|str| JSON.parse str}
maybe_json['nodes'] # method_missing in action
.map do |n|
n['new_field'] = 'new'
n
end
.continue(&:any?) # see documentation
.and_then do |new_nodes|
doc.data['nodes'] = new_nodes
doc.save!
end
end
In this example value
was no invoked as Maybe
was use more so for flow control.
Useful for catching a nil immediately
Just(nil) #=> Ronad::Just::CannotBeNil
never_nil = Just(5).and_then { nil } #=> Ronad::Just::CannotBeNil
good = ~Just(5) #=> 5
Similar to a null object pattern. Setting up defaults so #value
will never return nil. Can be
combined with a Maybe
.
d = ~Default('hello', nil) #=> 'hello'
d = ~Default('hello', Maybe(10)) #=> 10
Notice that default will recursively expand other Monads. As a general rule, you should never
receive a monad after invoking #value
.
Useful for delaying execution or not executing at all. Useful to combine with Default when it's not know if the default should execute.
person = Default(
Eventually { Person.create(name: "Frank", location: :unknown) },
Maybe(Person.find_by(name: "Frank"))
)
~ person.update(location: 'somewhere')
If find_by
finds a person the Person.create
will never be invoked.
Conversely if find_by
does not it will create the person and update their
location.
The gem is available as open source under the terms of the MIT License.