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