This is an intentional design choice to support transaction rollbacks, and altering the behavior would be a breaking change. Exceptions as flow-control is considered an antipattern, and Monads are considered an alternative to that practice.
There still is a need to adapt exception-laden code when wrapping them in monads, though.
You can work around this in a couple ways. The Try
monad is probably the best approach.
Alternately, you can reraise Halt
def my_method
yield Failiure('error code')
rescue Dry::Monads::Do::Halt
raise
rescue => err
Failure(err)
end
I can’t think of any reason why you would need to use yield inside a Try
monad. The purpose is to act as a boundary between exceptional code and monadic code. Simply moving the yield
outside of the Try
block should work fine
yield Try { raise 'something went wrong' }.to_result
Some times, using yield
is inconvenient for various reasons, and in those situations you can use the bind
interface directly.
Dry::Monads::Do.bind Failure('error code')
Basically, your code is responsible for transforming what it is doing into a monadic type, and then you hand it off to yield
. So for example, every Operation object that I use has this helper available:
# Utility helper to avoid .to_monad.or {} chaining
#
# @param [#to_monad] result, Dry::Monads::Result or object that responds to `to_monad`
# @param [Dry::Monads::Result::Failure, #to_proc, { :error => Symbol }] error
#
# @example With Yield
# user = yield fetch_user.(id).or { |err| Failure[:not_found, err] }
#
# @example With Either
# user = either fetch_user.(id), Failure(:not_found)
# user = either fetch_user.(id), ->(err) { Failure[:not_found, err] }
# user = either fetch_user.(ud), error: :not_found
#
# @raise [Dry::Monads::Do::Halt]
#
# @return the unwrapped successful value
def either(result, error)
failure =
case error
in { error: error_name }
proc { |err| Failure[error_name, err] }
in T::Procable
error
else
proc { error }
end
Dry::Monads::Do.bind result.to_monad.or(&failure)
end