Deterministic
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.
This gem is still WORK IN PROGRESS.
Usage
Success & Failure
Success(1).to_s # => "1"
Success(1) << Success(2) # => Success(2)
Success(Success(1)) # => Success(1)
Success(1).map { |v| v + 1} # => Success(2)
Success({a:1}).to_json # => '{"Success": {"a":1}}'
Failure(1).to_s # => "1"
Failure(1) << Failure(2) # => Failure(1)
Failure(Failure(1)) # => Failure(1)
Failure(1).map { |v| v + 1} # => Failure(2)
Failure({a:1}).to_json # => '{"Failure": {"a":1}}'
Either.attempt_all
The basic idea is to execute a chain of units of work and make sure all return either Success
or Failure
.
Either.attempt_all do
try { 1 }
try { |prev| prev + 1 }
end # => Success(2)
Take notice, that the result of of unit of work will be passed to the next one. So the result of prepare_somehing will be something in the second try.
If any of the units of work in between fail, the rest will not be executed and the last Failure
will be returned.
Either.attempt_all do
try { 1 }
try { raise "error" }
try { 2 }
end # => Failure(RuntimeError("error"))
However, the real fun starts if you use it with your own context. You can use this as a state container (meh!) or to pass a dependency locator:
class Context
attr_accessor :env, :settings
def some_service
end
end
# exemplary unit of work
module LoadSettings
def self.call(env)
settings = load(env)
settings.nil? ? Failure('could not load settings') : Success(settings)
end
def load(env)
end
end
Either.attempt_all(context) do
# this unit of work explicitly returns success or failure
# no exceptions are catched and if they occur, well, they behave as expected
# methods from the context can be accessed, the use of self for setters is necessary
let { self.settings = LoadSettings.call(env) }
# with #try all exceptions will be transformed into a Failure
try { do_something }
end
Pattern matching
Now that you have some result, you want to control flow by providing patterns.
#match
can match by
- success, failure, either or any
- values
- lambdas
- classes
Success(1).match do
success { |v| "success #{v}"}
failure { |v| "failure #{v}"}
either { |v| "either #{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
Combining #attempt_all
and #match
is the ultimate sophistication:
Either.attempt_all do
try { 1 }
try { |v| v + 1 }
end.match do
success(1) { |v| "We made it to step #{v}" }
success(2) { |v| "The correct answer is #{v}"}
end # => "The correct answer is 2"
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"
Flow control
Chaininig successfull actions
Success(1).and Success(2) # => Success(2)
Success(1).and_then { Success(2) } # => Success(2)
Success(1).or Success(2) # => Success(1)
Success(1).or_else { Success(2) } # => Success(1)
Chaininig failed actions
Failure(1).and Success(2) # => Failure(1)
Failure(1).and_then { Success(2) } # => Failure(1)
Failure(1).or Success(1) # => Success(1)
Failure(1).or_else { |v| Success(v)} # => Success(1)
The value or block result must always be a Success
or Failure
core_ext
You can use a core extension, to include Either in your own class or in Object, i.e. in all classes.
In a class, as a mixin
require "deterministic/core_ext/either" # this includes Deterministic in the global namespace!
class UnderTest
include Deterministic::CoreExt::Either
def test
attempt_all do
try { foo }
end
end
def foo
1
end
end
ut = UnderTest.new
ut.test # => Success(1)
To add it to all classes
require "deterministic/core_ext/object/either" # this includes Deterministic to Object
class UnderTest
def test
attempt_all do
try { foo }
end
end
def foo
1
end
end
ut = UnderTest.new
ut.test # => Success(1)
or use it on built-in classes
require "deterministic/core_ext/object/either"
h = {a:1}
h.attempt_all do
try { |s| s[:a] + 1}
end # => Success(2)
Maybe
The simplest NullObject wrapper there can be. It adds #some?
and #none?
to Object
though.
require 'deterministic/maybe' # you need to do this explicitly
Maybe(nil).foo # => None
Maybe(nil).foo. # => None
Mmaybe({a: 1})[:a] # => 1
Maybe(nil).none? # => true
Maybe({}).none? # => 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
null = Maybe.mimick(Mimick)
null.test # => None
null.foo # => NoMethodError
Inspirations
- My Monadic gem of course
#attempt_all
was somewhat inspired by An error monad in Clojure- Pithyless' rumblings
- either by rsslldnphy
- Functors, Applicatives, And Monads In Pictures
- Naught by avdi
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