WDYTA using dry-monad in repository?

One of the primary places I tend to use repos is within monadic operation objects, so yes this is a very useful thing to have. I retain the regular method interfaces though.

Here’s how:

T = Dry.Types(default: :strict)
M = Dry::Monads

# Automagically generate monadic interfaces for traditional methods
#
# @param [:result, :maybe] method_types the type of monad to wrap the expression in
#
# @example Result type
#   class Foo
#     extend MonadicMethods(:result)
#
#     def foo = "foo"
#     def bar = raise ArgumentError, "BarError"
#   end
#
#   Foo.new.foo  #=> "foo"
#   Foo.new.foo! #=> Success("foo")
#   Foo.new.bar! #=> Failure(ArgumentError)
#
# @example Maybe type
#   class Bar
#     extend MonadicMethods(:maybe)
#     def foo = "foo"
#     def bar = nil
#   end
#
#   Bar.new.foo  #=> "foo"
#   Bar.new.bar  #=> nil
#   Bar.new.foo? #=> Some("foo")
#   Bar.new.bar? #=> None()
#
# @raise [ArgumentError] unexpected monad type supplied
#
# @return Module
# rubocop:disable Naming/MethodName
def MonadicMethods(*method_types)
  T::Array.of(T::Symbol.enum(:maybe, :result))[method_types]

  Module.new do
    define_singleton_method(:inspect) { "MonadicMethods[#{method_types.map(&:inspect).join(", ")}]" }

    define_method :method_added do |method_name|
      unless /[?!]$/ =~ method_name
        if method_types.include?(:result)
          signature = :"#{method_name}!"

          remove_method signature if public_instance_methods.include?(signature)

          define_method signature do |*args, **kwargs, &block|
            M::Success(__send__(method_name, *args, **kwargs, &block))
          rescue => err
            M::Failure(err)
          end
        end

        if method_types.include?(:maybe)
          signature = :"#{method_name}?"

          remove_method signature if public_instance_methods.include?(signature)

          define_method signature do |*args, **kwargs, &block|
            M::Maybe(__send__(method_name, *args, **kwargs, &block))
          end
        end
      end
    end
  end
rescue Dry::Types::ConstraintError => err
  ArgumentError
    .new(err.message)
    .tap  { |arg| arg.set_backtrace(err.backtrace) }
    .then { |arg| raise arg }
end
1 Like