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 codeattribute;
- no big boilerplates involved as Mu::Resultis 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)