Ronad
Monads implemented in Ruby with sugar. Inspired by @tomstuart's talk https://codon.com/refactoring-ruby-with-monads
Installation
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
Sugar
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.
Usage
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
value, monad_value, ~
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 #=> trueMaybe
Maybe is useful as a safe navigation operator:
response = request(params) #=> {}
name = ~Maybe(response[:person])[:name] #=> nilIt'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] #=> nilThis 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
endIn this example value was no invoked as Maybe was use more so for flow control.
Just
Useful for catching a nil immediately
Just(nil) #=> Ronad::Just::CannotBeNil
never_nil = Just(5).and_then { nil } #=> Ronad::Just::CannotBeNil
good = ~Just(5) #=> 5Default
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)) #=> 10Notice that default will recursively expand other Monads. As a general rule, you should never
receive a monad after invoking #value.
Eventually
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.
License
The gem is available as open source under the terms of the MIT License.