monads in Ruby

notes on using dry-monads and dry-matcher.

definition of monad

monad is created by defining:

monads in dry-monads

monads

3 monads are available in dry-monads: Maybe, Either and Try - each having 2 types (subclasses):

monad methods

monads have the following methods:

tips

example of using dry-monads

require 'my_app/inject'

class Site::Create
  include Dry::Monads::Either::Mixin
  include Dry::Monads::Try::Mixin

  include MyApp::Inject['svcs.fetch_main_mirror', 'ops.create_site_setting']

  alias m method

  MAIN_MIRROR_NOT_FETCHED = 'error fetching main mirror'

  def after_create model, force_collect
    Right(model)
      .bind(m(:set_main_mirror))
      .bind(m(:_create_site_setting))
      .tee(m(:collect_products), force_collect)
  end

  def set_main_mirror model
    result = fetch_main_mirror.(model.domain)
    return Left(MAIN_MIRROR_NOT_FETCHED) if result.failure?

    Try do
      model.update!(domain: URL.host(result.value))
      model
    end.to_either
  end

  def _create_site_setting model
    result = create_site_setting.(site_id: model.id)

    if result.success?
      Right(model)
    else
      Left(operation_error_message(create_site_setting, result.value))
    end
  end

  def collect_products model, force_collect
    return Right(nil) unless model.data_source_SITE?
    CollectProductsJob.perform_later(model, force_collect)

    Right(nil)
  end
end
class MergeXpaths
  include Dry::Monads::Either::Mixin

  alias :m :method

  def call xpaths
    Right(xpaths)
      .fmap(m(:filter))
      .fmap(m(:sort))
      .value
      .join('|')
  end

  private

  def filter xpaths
    xpaths.select(&:present?).uniq
  end

  def sort xpaths
    xpaths.sort_by(&:size)
  end
end

example of using dry-matcher to match on result

require 'dry/matcher/either_matcher'

class OperationBase
  def raise_on_failure! result, model
    # works both with Either and Try values
    Dry::Matcher::EitherMatcher.(result) do |m|
      m.failure do |value|
        raise OperationError, error_message(value, model)
      end
    end
  end
end

in this simple case it’s possible to avoid using matcher at all:

class OperationBase
  def raise_on_failure! result, model
    if result.failure?
      raise OperationError, error_message(result.value, model)
    end
  end
end