Consequence
Simple monad implementation with clear and consistent syntax.
Usage
A monad has a value that it is wrapped around. Its value can be anything: String, Module, Proc etc... It takes its value as its only argument and can be initialized using the element reference syntax:
require 'consequence'
include Consequence
my_monad = Monad[4]
Its value can be retrieved with the #value
getter method.
my_monad.value # 4
To create new monad types, simply inherit from Monad
.
Foo = Class.new(Monad)
Bar = Class.new(Monad)
A monad is equal to another monad only if both it's type and value are equal.
Foo[0] == Foo[1] # false
Foo[0] == Bar[0] # false
Foo[0] == Foo[0] # true
Operations
Monads have two main operations >>
and <<
.
Both take a proc for their only argument. The supplied proc can take 0-2 arguments and the monad will match them, even if it's a lamda or method object with arity checking.
It's first argument will be passed the monads value:
Foo[4] << ->(v) { puts v }
# $ 4
It's second argument will be passed the monad itself. This is useful for making decisions based on the monads type.
Foo[0] << ->(v, m) { puts v if m.is_a?(Foo) }
# $ 0
>>
The >>
operation may be variously thought of as the 'map', 'bind' or 'join' operations, but has been left unnamed to avoid any specific connotations.
It takes the result of the proc given to it and passes it on:
Foo[0] >> ->(v) { Bar[v] } # Bar[0]
If it return's a result that is not a monad, it is wrapped up in one so the chain can continue:
Foo[0] >> ->(v) { v + 1 } # Foo[1]
<<
The <<
operation may be thought of as the 'pipe' operation.
It calls the proc given to it, but ignores it's return value:
Foo[0] << ->(v) { v + 1 } # Foo[0]
This is useful for creating 'side effects', such as logging or assigning instance variables:
Foo[0] << ->(v) { @side_effect = v + 1 } # Foo[0]
puts @side_effect
# $ 1
#to_proc
Before either operation calls a proc, the #to_proc
method is called on it. This can be useful if the operation is given an object that is no a proc, but has a #to_proc
method that returns one.
A good example of this is the Symbol object, whose #to_proc
method supplies a proc that sends the symbol as a message to its first argument. In this case this means calling that method on the value:
Foo[[1, 4, 7]] >> :pop # Foo[7]
This also can be used to make a composition syntax, by writing a #to_proc
method for a monad:
class Add < Monad
def to_proc
->(v) { v + value }
end
end
Add[1] >> Add[4] >> Add[6] # Add[11]
Built-In Types
NullMonad
The NullMonad
is a monad whose >>
and <<
operations have been overriden to ignore all input:
NullMonad[0] >> ->(v) { v + 1 } # Consequence::NullMonad[0]
This is different from the <<
operation, because the proc isn't even run, so can cause no side effects:
NullMonad[0] << ->(v) { @side_effect = v + 1 }
puts @side_effect.inspect
# $ nil
For both operations, the NullMonad
returns itself, so a chain of operations can be called on it without causing an error. This can be useful for stoping a chain midway through safely:
Continue = Class.new(Monad)
Stop = Class.new(NullMonad)
drop_first = ->(v) { v[1..-1] }
check_empty = ->(v, m) { v.empty? ? Stop[v] : m }
Continue[[1, 3, 4]] >> drop_first >> check_empty >> # Continue[[3, 4]]
drop_first >> check_empty >> # Continue[[4]]
drop_first >> check_empty >> # Stop[[]]
drop_first >> check_empty # Stop[[]]
Success & Failure
A Success
monad wraps up all exceptions in a Failure
monad:
Success[0] >> ->(v) { 5 / v }
# Consequence::Failure[#<ZeroDivisionError: divided by 0>]
A Failure
monad is a subclass of the NullMonad
so all successive chained procs are ignored.
Both Success
and Failure
respond to the #succeeded?
and #failed?
query methods in the way you'd expect:
Success[0].succeeded? # true
Success[0].failed? # false
Failure[0].succeeded? # false
Failure[0].failed? # true
For an example, check out the Success & Failure example on the wiki.
Something & Nothing
A Something
monad wraps up a nil result in a Nothing
monad:
Something[[1, 3, 5]] >> ->(v) { v[4] } # Consequence::Nothing[nil]
A Nothing
monad is also a subclass of the NullMonad
so all successive chained procs are ignored. This prevents MissingMethod
errors from trying to call a method on a nil
.
A Nothing
responds positively to the #nil?
method:
Nothing[nil].nil? # true
DelegatesToValue
DelegatesToValue
is a module that can be included into Monad that adds a #method_missing
method to delegate calls to its value:
Foo.include DelegatesToValue
Foo[[1, 4, 6]].map {|n| n + 1} # Foo[[2, 5, 7]]
It delegates via the >>
operation, so subclasses of the NullMonad will respond to delegated method calls, but still take no action:
Something.include DelegatesToValue
Nothing.include DelegatesToValue
dangrous_hash = {user: {orders: {1 => {price: 3.99} } } }
Something[dangrous_hash][:user][:orders][1][:price] # Consequence::Something[3.99]
Something[dangrous_hash][:user][:orders][2][:price] # Consequence::Nothing[nil]
Wiki
To find some examples and information about utils, check out the wiki and feel free to contribute to it.
Inspirations
Installation
Add this line to your application's Gemfile:
gem 'consequence'
Contributing
- Fork it ( https://github.com/[my-github-username]/consequence/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request