DEV Community

Cover image for Experimenting simpler execution flows in Ruby
olistik
olistik

Posted on • Edited on

Experimenting simpler execution flows in Ruby

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)}"
Enter fullscreen mode Exit fullscreen mode

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.

FTW

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)