Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another alternative (and backwards-compatible) approach to Outcome class : Dry::Monads::Result #123

Open
1 of 4 tasks
saverio-kantox opened this issue Jan 26, 2018 · 1 comment

Comments

@saverio-kantox
Copy link
Contributor

So there is this dry-rb/dry-monads library that has lots of goodies. One that is really useful for mutations is the Result monad, which has two subclasses Success and Failure.

I propose (and I can actually provide the PR) to replace Outcome with subclasses of Success and Failure, provided that:

  • They already respond to #success?.
  • Mutations::Success should alias success to result (with no arguments)
  • Mutations::Failure should alias failure to errors
  • Both Mutations::Success and Mutations::Failure should include a phony module Mutations::Outcome - so .is_a? and === will still work.

With the previous additions, they are completely backwards-compatible, and it would be some 50 LOC.

Having Outcomes based on monads adds this optional behavior (the first one was asked in #102):

# result method with two args, provided by Dry::Monads::Result
outcome.result(
  ->(error) { puts "Error: #{error.inspect}" },
  ->(result) { puts "Result: #{result.inspect}" }
)

# `bind`, `fmap` and `or` chain other code or return the receiver, depending on success:

# bind returns the result of the block - for instance another outcome, or something else
successful_outcome.bind { |result| 1 }
# => 1
failed_outcome.bind { |result| 1 }
# => failed_outcome

# fmap wraps the result inside the same type
successful_outcome.fmap { |result| 1 }
# => Outcome::Success<value=1>
failed_outcome.fmap { |result| 1 }
# => failed_outcome

# `or` chain other code provided it's a failure:
successful_outcome.or { |result| 1 }
# => successful_outcome
failed_outcome.or { |result| 1 }
# => 1

# `or_fmap` chain other code provided it's a failure, and wraps in a success:
successful_outcome.or_fmap { |result| 1 }
# => successful_outcome
failed_outcome.or_fmap { |result| 1 }
# => Outcome::Success<value=1>

plus many more:

  • value_or returns the result on success, executes the block on failure
  • flip flips between success and failure
  • All these methods accept any object responding to call instead of a block, plus any number of additional arguments that would be appended to the block/callable.

If Mutation::Command subclasses could additionally respond to call (by aliasing it to run), they would allow constructs like:

MyCommand.(inital_args)
  .bind(MyOtherCommand)
  .bind(StillAnotherCommand, extra: "params")

All without breaking current code depending on run, run! and Outcome.

@saverio-kantox
Copy link
Contributor Author

This can be also a peer-dependency, so if Dry::Monads is loaded, the alternative implementation steps in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant