I created the gem Mu::Result because I feel the need to pass Result objects instead of raw values.
This allows me to write this kind of code:
def fetch(post_id:, author_id:)
result = api_call("https://foo.com/posts/#{post_id}")
return result if result.error?
post = result.unwrap
result = api_call("https://foo.com/authors/#{author_id}")
return result if result.error?
author = result.unwrap
Result.success(author: author, post: post)
end
result = fetch(post_id: 23, author_id: 42)
if result.error?
case result.code
when :network_error then puts 'Some troubles in the network'
when :bad_request then puts 'Maybe a malformed request'
when :forbidden then puts 'Check your credentials'
when :not_found then puts 'They are gone, Jim'
else
puts 'Generic errors'
end
exit 1
end
puts "The post is: #{result.unwrap(:body)}"
with some wins like:
- it's relatively easy to propagate errors without relying on exceptions;
- there's a common API to handle richer return values;
- pattern matching can be performed with a simple case statement against result's
code
attribute; - no big boilerplates involved as
Mu::Result
is really tiny.
Yet, I feel it can be improved even further. 🤓
Taking heavy inspiration from dry-rb's do notation here's a draft implementation I came up:
require 'mu/result'
module Mu
module Flow
def self.do(stuff, *args)
stuff.call(*args) do |step|
return step if step.error?
step
end
end
def Success(data = nil)
Mu::Result.success(data)
end
def Error(data = nil)
Mu::Result.error(data)
end
def Flow(method_name, *args)
Mu::Flow.do(method(method_name), *args)
end
end
end
Everyday code can use it this way:
include Mu::Flow
def logic(n)
age = yield Success(n / 2)
yield Error(age.unwrap).code!(:too_low) if age.unwrap > 10
magic_number = yield Success(age.unwrap ** 2)
yield magic_number.code!(:wow) if magic_number.unwrap > 50
magic_number
end
# simpler way with the helper:
result = Flow(:logic, 18)
puts "result: #{result.inspect}"
# result: #<Mu::Result::BaseResult:0x00007fd3d8027520 @code=:wow, @data=81>
# or more explicitly:
result = Mu::Flow.do(method(:logic), 8)
puts "result: #{result.inspect}"
# result: #<Mu::Result::BaseResult:0x00007fd3d8027160 @code=:ok, @data=16>
The example is contrived but it shows that there's no need to write explicit returns for error results anymore.
The implementation of Mu::Flow
aims to remain tiny.
I'm still experimenting with this approach to see whether it produces a tangible gain or not. 🧐
Top comments (0)