Build Status

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.