ToResult is a wrapper built over dry-monads
to make the Do Notation
, Result
and Try
concepts more handy and consistent to use, especially to implement the Railway Pattern.
dry-monads
requires to write boilerplate code everytime we want a method to return a Success
or Failure
, for example:
def my_method
Success(another_method.call)
rescue StandardError => e
Failure(e)
end
yes, we can use Try
which makes the code easier to read and faster to write:
def my_method
Try do
another_method.call
end.to_result
end
but I feel like to_result
is not really visible at the end of the block and if you forget to write it (as always happens to me) your application blows up.
But this is not the biggest problem, bear with me.
One of the biggest problem is that we cannot use the Do Notation inside a Try
block:
# this will return a Failure(Dry::Monads::Do::Halt)
def my_method
Try do
yield Failure('error code')
end.to_result
end
and you cannot even use yield
and rescue
in the same method:
# this will return a Failure(Dry::Monads::Do::Halt)
def my_method
yield Failure('error code')
rescue StandardError => e
# e is an instance of Dry::Monads::Do::Halt
Failure(e)
end
because they will raise a Dry::Monads::Do::Halt
exception and the original error will be forever lost if we do not "unbox" the exception with e.result
like this:
def my_method
yield Failure('error code')
rescue Dry::Monads::Do::Halt => e
return e.result
rescue StandardError => e
Failure(e)
end
to be honest this is an implementation detail I don't want to care about while I'm writing my business logic and as far as I've seen this is really hard for junior developers to figure out what is happening with Do::Halt
.
With this gem, to-result
, that piece of code can be written as:
def my_method
ToResult do
yield Failure('error code')
end
end
and it will return Failure('error code')
without all the effort to think about Do::Halt
. Moreover, you can keep using ToResult
everytime you could have used Try
or monads in general, so you have just one way to write monads in your code.
To install with bundler:
bundle add to-result
or with gem
:
gem install to-result
To use it with instances of a class, just include it
require 'to_result'
class MyClass
include ToResultMixin
def my_method
ToResult do
whatever_method.call
end
end
end
or if you want to use it with Singleton Classes:
require 'to_result'
class MyClass
extend ToResultMixin
class << self
def my_method
ToResult do
whatever_method.call
end
end
end
end
now you can always use ToResult
all the time you wanted to use Try
or return Success/Failure
but with a more convenient interface and consistent behaviour, my goal is to have a solution that can be used for every use-case.
Look at this:
ToResult { raise StandardError.new('error code') }
# returns Failure(StandardError('error code'))
ToResult { yield Success('hello!') }
# returns Success('hello!')
ToResult { yield Failure('error code') }
# returns Failure('error code')
ToResult { yield Failure(StandardError.new('error code')) }
# returns Failure(StandardError('error code'))
ToResult(only: [YourCustomError]) { yield Failure(YourCustomError.new('error code')) }
# returns Failure(YourCustomError('error code'))
ToResult(only: [ArgumentError]) { yield Failure(YourCustomError.new('error code')) }
# raises YourCustomError('error code')
to-result gives you the possibility to define a callback to be called when an error is raised inside the ToResult
block, this is a handy place to log errors.
You can define a global callback, usually defined into an initializer:
# initializers/to_result.rb
ToResultMixin.configure do |c|
c.on_error = Proc.new { |e| Logger.log_error(e) }
end
or a local callback:
ToResult(on_error: proc { |e| Logger.log_error(e) }) do
yield Failure(StandardError.new('error code'))
end
you can even use both at the same time but keep in mind that local callback overrides the global one.
I'm already planning to implement some useful features:
- write more examples/documentation/tests
- configurable error logging when an exception is catched inside
DoResult
e.g. sending the log to Airbrake or whathever service you are using - transform/process the catched error => this can be handled with
alt_map
or other methods already available indry-monads
- any type of suggestion is appreciated 😁
gem build to-result
gem push to-result-<version>.gem