Lab42::Result
A result encapsulation class, like the Either
type in Haskell.
Context Quick Starting Guide
Given
require "lab42/result/autoimport"
let(:ok) { Result.ok(42) }
let(:error) { Result.error("oh no!") }
Accessing a result
If OK
Example: It's ok
expect( ok ).to be_ok
Example: Its value might be of interest
expect( ok.value ).to eq(42)
And it will execute the block passed to the if_ok
method
x = nil
expect(ok.if_ok {x = 42}).to eq(42)
expect(x).to eq(42)
And the value is passed in
expect(ok.if_ok{ _1/2}).to eq(21)
But not the one passed to the if_error
method
x = nil
expect(ok.if_error {x = 42}).to be_nil
expect(x).to be_nil
Example: And will not raise any error
expect( ok.raise! ).to eq(42)
And the same holds for raise!
with a replacement Exception
expect( ok.raise!(ArgumentError) ).to eq(42)
If Error
Example: It's h(error)
expect( error ).not_to be_ok
Example: Its value might (still) be of interest
expect( error.value ).to eq("oh no!")
Example: And will certainly raise this time
expect{ error.raise! }.to raise_error(RuntimeError, "oh no!")
And as often times you will match on an error case only and raise a custom exception the following shortcut comes in handy
expect{ error.raise!(KeyError) }.to raise_error(KeyError, "oh no!")
And also you might like to have access to the original message
expect{ error.raise!(KeyError) { "key not found #{_1}"} }
.to raise_error(KeyError, "key not found oh no!")
And it will execute the block passed to the if_error
method
x = nil
expect(error.if_error {x = 42}).to eq(42)
expect(x).to eq(42)
And the error and message are passed in
expect(error.if_error {[_1, _2]}).to eq([RuntimeError, "oh no!"])
But not the one passed to the if_ok
method
x = nil
expect(error.if_ok {x = 42}).to be_nil
expect(x).to be_nil
Capturing Exceptions
If you have a piece of code like this:
Result.ok(some_computation)
rescue MyError
Result.error("Oh my", error: MyError)
you can replace it with the convenient
Result.from("Oh my") {some_computation}
Given
def divide by
Result.from("Zero Division") { 100 / by }
end
Example: Zero Division, not a problem anymore
error = divide(0)
expect( error ).not_to be_ok
expect( error.status ).to eq(ZeroDivisionError)
Example: Correct Division still works
divide(4) in [_, value]
expect( value ).to eq(25)
Context A More Detailed View
Ok without a value
Sometimes all we want is an :ok
to get back or :error
with a message, than we can use
the default value of nil
Example: Nil is default for ok
expect(Result.ok.value).to be_nil
expect( Result.ok.ok? ).to be_truthy
Error with a different exception
On the other hand you might not want to raise a RuntimeError
all the time, that is
when the optional keyword parameter error:
comes in handy
Example: Error with an Argument
expect{ Result.error("ooops", error: ArgumentError).raise!}
.to raise_error(ArgumentError, "ooops")
Context Pattern Matching
Given
let(:my_error) {Class.new(RuntimeError)}
let(:message) {"That was bad"}
let(:surprise) {"Not 42"}
let(:ok) {Result.ok(surprise)}
let(:error) {Result.error(message, error: my_error)}
def match result
case result
in [:ok, value]
value
in [my_error, message]
message
end
end
Example: Matching The Good
expect( match(ok) ).to eq(surprise)
Example: Matching The Bad (no, there will be no Ugly)
expect( match(error) ).to eq(message)
Example: If the Exception I do not want
error in [_, error_message]
expect( error_message ).to eq(message)
Context Saveguards
And last but not least, to assure that all instances of Result
are frozen we have removed the
default constructor (we have not - yet - shadowed Object#allocate
though)
Example: Look Mam, no default constructor!
expect{ Result.new }.to raise_error(NoMethodError)
LICENSE
Copyright 2020 Robert Dober robert.dober@gmail.com
Apache-2.0 c.f LICENSE