module Flows::Result::Do

Do-notation for Result Objects.

This functionality aims to simplify common control flow pattern: when you have to stop execution on a first failure and return this failure. Do Notation inspired by [Do Notation in dry-rb](dry-rb.org/gems/dry-monads/1.3/do-notation/) and [Haskell do keyword](wiki.haskell.org/Keywords#do).

Sometimes you have to write something like this:

class Something
  include Flows::Result::Helpers

  def perform
    user_result = fetch_user
    return user_result if user_result.err?

    data_result = fetch_data
    return data_result if data_result.err?

    calculation_result = calculation(user_result.unwrap[:user], data_result.unwrap)
    return calculation_result if user_result.err?

    ok(data: calculation_result.unwrap[:some_field])
  end

  private

  def fetch_user
    # returns Ok or Err
  end

  def fetch_data
    # returns Ok or Err
  end

  def calculation(_user, _data)
    # returns Ok or Err
  end
end

The main idea of the code above is to stop method execution and return failed Result Object if one of the sub-operations is failed. At the moment of failure.

By using Do Notation feature you may rewrite it like this:

class SomethingWithDoNotation
  include Flows::Result::Helpers
  extend Flows::Result::Do # enable Do Notation

  do_notation(:perform) # changes behaviour of `yield` in this method
  def perform
    user = yield(fetch_user)[:user] # yield here returns array of one element
    data = yield fetch_data # yield here returns a Hash

    ok(
      data: yield(calculation(user, data))[:some_field]
    )
  end

  # private method definitions
end

`do_notation(:perform)` makes some wrapping here and allows you to use `yield` inside the `perform` method in a non standard way: to unpack results or instantly leave a method if a failed result was provided.

## How to use it

First of all, you have to include `Flows::Result::Do` mixin into your class or module. It adds `do_notation` class method. `do_notation` accepts method name as an argument and changes behaviour of `yield` inside this method. By the way, when you are using `do_notation` you cannot pass a block to modified method anymore.

class MyClass
  extend Flows::Result::Do

  do_notation(:my_method_1)
  def my_method_1
    # some code
  end

  do_notation(:my_method_2)
  def my_method_2
    # some code
  end
end

`yield` in such methods is working by the following rules:

ok_result = Flows::Result::Ok.new(a: 1, b: 2)
err_result = Flows::Result::Err.new(x: 1, y: 2)

# the following three lines are equivalent
yield(ok_result)
ok_result.unwrap
{ a: 1, b: 2 }

# the following three lines are equivalent
yield(:a, :b, ok_result)
ok_result.unwrap.values_at(:a, :b)
[1, 2]

# the following three lines are equivalent
return err_result
yield(err_result)
yield(:x, :y, err_result)

As you may see, `yield` has two forms of usage:

## How it works

Under the hood `Flows::Result::Do` creates a module and prepends it to your class or module. Invoking `do_notation(:method_name)` adds special wrapper method to the prepended module. So, when you perform call to `YourClassOrModule#method_name` - you're executing wrapper in the prepended module.

Constants

MOD_VAR_NAME
SingletonVarsSetup

Public Instance Methods

do_notation(method_name) click to toggle source
# File lib/flows/result/do.rb, line 165
def do_notation(method_name)
  prepended_mod = Util.fetch_and_prepend_module(self)

  Util.define_wrapper(prepended_mod, method_name)
end