Resonad
Lightweight, functional "result" objects that can be used instead of exceptions.
Read: Result Objects - Errors Without Exceptions
Typical Usage Example
Assuming each method returns a Resonad
(Ruby 2.7 syntax):
find_widget(widget_id)
.and_then { update_widget(_1) }
.on_success { logger.info("Updated #{_1}" }
.on_failure { logger.warn("Widget update failed because #{_1}") }
Success Type
A value that represents success. Wraps a value
that can be any arbitrary
object.
result = Resonad.Success(5)
result.success? #=> true
result.failure? #=> false
result.value #=> 5
result.error #=> raises an exception
Failure Type
A value that represents a failure. Wraps an error
that can be any arbitrary
object.
result = Resonad.Failure(:buzz)
result.success? #=> false
result.failure? #=> true
result.value #=> raises an exception
result.error #=> :buzz
Mapping
Non-destructive update for the value
of a Success
object. Does nothing to
Failure
objects.
The block takes the value
as an argument, and returns the new value
.
result = Resonad.Success(5)
.map { _1 + 1 } # 5 + 1 -> 6
.map { _1 + 1 } # 6 + 1 -> 7
.map { _1 + 1 } # 7 + 1 -> 8
result.success? #=> true
result.value #=> 8
result = Resonad.Failure(:buzz)
.map { _1 + 1 } # not run
.map { _1 + 1 } # not run
.map { _1 + 1 } # not run
result.success? #=> false
result.error #=> :buzz
Aliases
Lots of the Resonad methods have aliases.
Personally, I can never remember if it's success?
or successful?
or ok?
,
so let's just do it the Ruby way and allow all of them.
# >>> object creation aliases (same for failure) <<<
result = Resonad.Success(5)
result = Resonad.success(5) # lowercase, for those offended by capital letters
result = Resonad::Success[5] # class constructor method
# >>> success aliases <<<
result.success? #=> true
result.successful? #=> true
result.ok? #=> true
# >>> failure aliases <<<
result.failure? #=> false
result.failed? #=> false
result.bad? #=> false
# >>> mapping aliases <<<
result.map { _1 + 1 } #=> Success(6)
result.map_value { _1 + 1 } #=> Success(6)
# >>> flat mapping aliases <<<
result.and_then { Resonad.Success(_1 + 1) } #=> Success(6)
result.flat_map { Resonad.Success(_1 + 1) } #=> Success(6)
# >>> error flat mapping aliases <<<
result.or_else { Resonad.Failure(_1 + 1) } # not run
result.otherwise { Resonad.Failure(_1 + 1) } # not run
result.flat_map_error { Resonad.Success(_1 + 1) } # not run
# >>> conditional tap aliases <<<
# pattern: (on_|if_|when_)(success_alias|failure_alias)
result.on_success { puts "hi" } # outputs "hi"
result.if_success { puts "hi" } # outputs "hi"
result.when_success { puts "hi" } # outputs "hi"
result.on_ok { puts "hi" } # outputs "hi"
result.if_ok { puts "hi" } # outputs "hi"
result.when_ok { puts "hi" } # outputs "hi"
result.on_successful { puts "hi" } # outputs "hi"
result.if_successful { puts "hi" } # outputs "hi"
result.when_successful { puts "hi" } # outputs "hi"
result.on_failure { puts "hi" } # not run
result.if_failure { puts "hi" } # not run
result.when_failure { puts "hi" } # not run
result.on_bad { puts "hi" } # not run
result.if_bad { puts "hi" } # not run
result.when_bad { puts "hi" } # not run
result.on_failed { puts "hi" } # not run
result.if_failed { puts "hi" } # not run
result.when_failed { puts "hi" } # not run
Flat Mapping (a.k.a. and_then
)
Non-destructive update for a Success
object. Either turns it into another
Success
(can have a different value
), or turns it into a Failure
. Does
nothing to Failure
objects.
The block takes the value
as an argument, and returns a Resonad
(either
Success
or Failure
).
result = Resonad.Success(5)
.and_then { Resonad.Success(_1 + 1) } # updates to Success(6)
.and_then { Resonad.Failure("buzz #{_1}") } # updates to Failure("buzz 6")
.and_then { Resonad.Success(_1 + 1) } # not run (because it's a failure)
.error #=> "buzz 6"
# also has a less-friendly but more-technically-descriptive alias: `flat_map`
result.flat_map { Resonad.Success(_1 + 1) }
This is different to Ruby's #then
method added in 2.6. The block for #then
would take a Resonad argument, regardless of whether it's Success
or
Failure
. The block for #and_then
takes a Success
object's value, and
only runs on Success
objects, not Failure
objects.
Error Mapping
Just as Success
objects can be chained with #map
and #and_then
, so can
Failure
objects with #map_error
and #or_else
. This isn't used as often,
but has a few use cases such as:
# Use Case: convert an error value into another error value
make_http_request #=> Failure(404)
.map_error { |status_code| "HTTP #{status_code} Error" }
.error #=> "HTTP 404 Error"
# Use Case: recover from error, turning into Success
load_config_file #=> Failure(:config_file_missing)
.or_else { try_recover_from(_1) }
.value #=> { :setting => 'default' }
def try_recover_from(error)
if error == :config_file_missing
Resonad.Success({ setting: 'default' })
else
Resonad.Failure(error)
end
end
Conditional Tap
If you're in the middle of a long chain of methods, and you don't want to break
the chain to run some kind of side effect, you can use the #on_success
and
#on_failure
methods. These run an arbitrary block code, but do not affect the
result object in any way. They work like Ruby's #tap
method, but Failure
objects will not run on_success
blocks, and Success
objects will not run
on_failure
blocks.
do_step_1
.and_then { do_step_2(_1) }
.and_then { do_step_3(_1) }
.on_success { puts "Successful step 3 result: #{_1}" }
.and_then { do_step_4(_1) }
.and_then { do_step_5(_1) }
.on_failure { puts "Uh oh! Step 5 failed: #{_1} }
.and_then { do_step_6(_1) }
.and_then { do_step_7(_1) }
There are lots of aliases for these methods. See the "Aliases" section above.
Pattern Matching Support
If you are using Ruby 2.7 or later, you can pattern match on Resonad objects. For example:
case result
in { value: } # match any Success
puts value
in { error: :not_found } # match Failure(:not_found)
puts "Thing not found"
in { error: String => msg } # match any Failure with a String error
puts "Failed to fetch thing because #{msg}"
in { error: } # match any Failure
raise "Unhandled error: #{error.inspect}"
end
Resonad.Success(5)
deconstructs to:
- Hash:
{ value: 5 }
- Array:
[:success, 5]
And Resonad.Failure('yikes')
deconstructs to:
- Hash:
{ error: 'yikes' }
- Array:
[:failure, 'yikes']
Automatic Exception Rescuing
If no exception is raised, wraps the block's return value in Success
. If an
exception is raised, wraps the exception object in Failure
.
def try_divide(top, bottom)
Resonad.rescuing_from(ZeroDivisionError) { top / bottom }
end
yep = try_divide(6, 2)
yep.success? #=> true
yep.value #=> 3
nope = try_divide(6, 0)
nope.success? #=> false
node.error #=> #<ZeroDivisionError: ZeroDivisionError>
Convenience Mixin
If you're tired of typing "Resonad." in front of everything, you can include
the Resonad::Mixin
mixin.
class RobotFortuneTeller
include Resonad::Mixin
def next_fortune
case rand(0..100)
when 0..70
# title-case constructor from Resonad::Mixin
Success("today is auspicious")
when 71..95
# lower-case constructor from Resonad::Mixin
success("ill omens abound")
else
# direct access to classes from Resonad::Mixin
Failure.new("MALFUNCTION")
end
end
end
Note that Resonad::Mixin
provides private methods, and private constants, so
you can't do this:
RobotFortuneTeller.new.Success(5)
#=> NoMethodError: private method `Success' called for #<RobotFortuneTeller:0x00007fe7fc0ff0c8>
RobotFortuneTeller::Success
#=> NameError: private constant Resonad::Mixin::Success referenced
If you want the methods/constants to be public, then use Resonad::PublicMixin
instead.
Contributing
Bug reports and pull requests are welcome on GitHub at: https://github.com/tomdalling/resonad
I'm open to PRs that make the gem more convenient, or that makes calling code read better.
Make sure your PR has full test coverage.