Deterministic
Deterministic is to help your code to be more confident, it's specialty is flow control of actions that either succeed or fail.
This is a spiritual successor of the Monadic gem. The goal of the rewrite is to get away from a bit to forceful aproach I took in Monadic, especially when it comes to coercing monads, but also a more practical but at the same time more strict adherence to monad laws.
Patterns
Deterministic provides different monads, here is a short guide, when to use which
Result: Success & Failure
- an operation which can succeed or fail
- the result (content) of of the success or failure is important
- you are building one thing
- chaining: if one fails (Failure), don't execute the rest
Option: Some & None
- an operation which returns either some result or nothing
- in case it returns nothing it is not important to know why
- you are working rather with a collection of things
- chaining: execute all and then select the successful ones (Some)
Either: Left & Right
- an operation which returns several good and bad results
- the results of both are important
- chaining: if one fails, continue, the content of the failed and successful are important
Maybe
- an object may be nil, you want to avoid endless nil? checks
Usage
Result: Success & Failure
Success(1).to_s # => "1"
Success(Success(1)) # => Success(1)
Failure(1).to_s # => "1"
Failure(Failure(1)) # => Failure(1)
Maps a Result
with the value a
to the same Result
with the value b
.
Success(1).fmap { |v| v + 1} # => Success(2)
Failure(1).fmap { |v| v - 1} # => Failure(0)
Maps a Result
with the value a
to another Result
with the value b
.
Success(1).bind { |v| Failure(v + 1) } # => Failure(2)
Failure(1).bind { |v| Success(v - 1) } # => Success(0)
Maps a Success
with the value a
to another Result
with the value b
. It works like #bind
but only on Success
.
Success(1).map { |n| Success(n + 1) } # => Success(2)
Failure(0).map { |n| Success(n + 1) } # => Failure(0)
Maps a Failure
with the value a
to another Result
with the value b
. It works like #bind
but only on Failure
.
Failure(1).map_err { |n| Success(n + 1) } # => Success(2)
Success(0).map_err { |n| Success(n + 1) } # => Success(0)
Success(0).try { |n| raise "Error" } # => Failure(Error)
Replaces Success a
with Result b
. If a Failure
is passed as argument, it is ignored.
Success(1).and Success(2) # => Success(2)
Failure(1).and Success(2) # => Failure(1)
Replaces Success a
with the result of the block. If a Failure
is passed as argument, it is ignored.
Success(1).and_then { Success(2) } # => Success(2)
Failure(1).and_then { Success(2) } # => Failure(1)
Replaces Failure a
with Result
. If a Failure
is passed as argument, it is ignored.
Success(1).or Success(2) # => Success(1)
Failure(1).or Success(1) # => Success(1)
Replaces Failure a
with the result of the block. If a Success
is passed as argument, it is ignored.
Success(1).or_else { Success(2) } # => Success(1)
Failure(1).or_else { |n| Success(n)} # => Success(1)
Executes the block passed, but completely ignores its result. If an error is raised within the block it will NOT be catched.
Success(1).try { |n| log(n.value) } # => Success(1)
The value or block result must always be a Result
i.e. Success
or Failure
.
Result Chaining
You can easily chain the execution of several operations. Here we got some nice function composition.
The method must be a unary function, i.e. it always takes one parameter - the context, which is passed from call to call.
The following aliases are defined
alias :>> :map
alias :>= :try
alias :** :pipe # the operator must be right associative
This allows the composition of procs or lambdas and thus allow a clear definiton of a pipeline.
Success(params) >> validate >> build_request ** log >> send ** log >> build_response
Complex Example in a Builder Class
class Foo
include Deterministic
alias :m :method # method conveniently returns a Proc to a method
def call(params)
Success(params) >> m(:validate) >> m(:send)
end
def validate(params)
# do stuff
Success(validate_and_cleansed_params)
end
def send(clean_params)
# do stuff
Success(result)
end
end
Foo.new.call # Success(3)
Chaining works with blocks (#map
is an alias for #>>
)
Success(1).map {|ctx| Success(ctx + 1)}
it also works with lambdas
Success(1) >> ->(ctx) { Success(ctx + 1) } >> ->(ctx) { Success(ctx + 1) }
and it will break the chain of execution, when it encounters a Failure
on its way
def works(ctx)
Success(1)
end
def breaks(ctx)
Failure(2)
end
def never_executed(ctx)
Success(99)
end
Success(0) >> method(:works) >> method(:breaks) >> method(:never_executed) # Failure(2)
#map
aka #>>
will not catch any exceptions raised. If you want automatic exception handling, the #try
aka #>=
will catch an error and wrap it with a failure
def error(ctx)
raise "error #{ctx}"
end
Success(1) >= method(:error) # Failure(RuntimeError(error 1))
Pattern matching
Now that you have some result, you want to control flow by providing patterns.
#match
can match by
- success, failure, result or any
- values
- lambdas
- classes
Success(1).match do
success { |v| "success #{v}"}
failure { |v| "failure #{v}"}
result { |v| "result #{v}"}
end # => "success 1"
Note1: the inner value has been unwrapped!
Note2: only the first matching pattern block will be executed, so order can be important.
The result returned will be the result of the first #try
or #let
. As a side note, #try
is a monad, #let
is a functor.
Values for patterns are good, too:
Success(1).match do
success(1) {|v| "Success #{v}" }
end # => "Success 1"
You can and should also use procs for patterns:
Success(1).match do
success ->(v) { v == 1 } {|v| "Success #{v}" }
end # => "Success 1"
Also you can match the result class
Success([1, 2, 3]).match do
success(Array) { |v| v.first }
end # => 1
If no match was found a NoMatchError
is raised, so make sure you always cover all possible outcomes.
Success(1).match do
failure(1) { "you'll never get me" }
end # => NoMatchError
A way to have a catch-all would be using an any
:
Success(1).match do
any { "catch-all" }
end # => "catch-all"
core_ext
You can use a core extension, to include Result in your own class or in Object, i.e. in all classes.
require 'deterministic/core_ext/object/result'
[1].success? # => false
Success(1).failure? # => false
Success(1).success? # => true
Failure(1).result? # => true
Option
Some(1).some? # #=> true
Some(1).none? # #=> false
None.some? # #=> false
None.none? # #=> true
Some(1).fmap { |n| n + 1 } # => Some(2)
None.fmap { |n| n + 1 } # => None
Some(1).map { |n| Some(n + 1) } # => Some(2)
Some(1).map { |n| None } # => None
None.map { |n| Some(n + 1) } # => None
Some(1).value # => 1
Some(1).value_or(2) # => 1
None.value # => NoMethodError
None.value_or(0) # => 0
Some(1).value_to_a # => Some([1])
Some([1]).value_to_a # => Some([1])
None.value_to_a # => None
Some(1) + Some(1) # => Some(2)
Some([1]) + Some(1) # => TypeError: No implicit conversion
None + Some(1) # => Some(1)
Some(1) + None # => Some(1)
Some([1]) + None + Some([2]) # => Some([1, 2])
Coercion
Option.any?(nil) # => None
Option.any?([]) # => None
Option.any?({}) # => None
Option.any?(1) # => Some(1)
Option.some?(nil) # => None
Option.some?([]) # => Some([])
Option.some?({}) # => Some({})
Option.some?(1) # => Some(1)
Option.try! { 1 } # => Some(1)
Option.try! { raise "error"} # => None
Pattern Matching
Some(1).match {
some(1) { |n| n + 1 }
some { 1 }
none { 0 }
} # => 2
Maybe
The simplest NullObject wrapper there can be. It adds #some?
and #null?
to Object
though.
require 'deterministic/maybe' # you need to do this explicitly
Maybe(nil).foo # => Null
Maybe(nil).foo. # => Null
Maybe({a: 1})[:a] # => 1
Maybe(nil).null? # => true
Maybe({}).null? # => false
Maybe(nil).some? # => false
Maybe({}).some? # => true
Mimic
If you want a custom NullObject which mimicks another class.
class Mimick
def test; end
end
naught = Maybe.mimick(Mimick)
naught.test # => Null
naught.foo # => NoMethodError
Inspirations
- My Monadic gem of course
#attempt_all
was somewhat inspired by An error monad in Clojure (attempt all has now been removed)- Pithyless' rumblings
- either by rsslldnphy
- Functors, Applicatives, And Monads In Pictures
- Naught by avdi
- Rust's Result
Installation
Add this line to your application's Gemfile:
gem 'deterministic'
And then execute:
$ bundle
Or install it yourself as:
$ gem install deterministic
Contributing
- Fork it
- 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 new Pull Request