Hey,
I’m interested in your opinion? So far I’ve used dry-monads just in interactors. Now I have some API’s (i.e. http), which I hide behind a repository:
i.e.:
class Repo
attr_reader :conn
def initialize
endpoint = Payments::Slice[:settings].api_endpoint
@conn = Faraday.new(url: endpoint) do |conn|
conn.response :logger
conn.response(
:json,
:content_type => "application/json",
:parser_options => { :symbolize_names => true },
)
end
end
def filtered(**filter)
get("customers", filter)
end
private
def get(path, params = {})
response = conn.get(path, params)
if (response.success?)
response.body
else
raise Repo::Errors::BasicError, "Basic Error at Repo: #{response.status} #{response.body}"
end
end
end
With sql-repos ((i.e. rom-rb, rom-sql)), I don’t feel like dry-monads would offer me great benefit, but in the above case it is tempting to change the code to following:
i.e.:
class Repo
attr_reader :conn
def initialize
endpoint = Payments::Slice[:settings].api_endpoint
@conn = Faraday.new(url: endpoint) do |conn|
conn.response :logger
conn.response(
:json,
:content_type => "application/json",
:parser_options => { :symbolize_names => true },
)
end
end
def filtered(**filter)
customers = yield get("customers", filter)
Success(customers)
end
private
def get(path, params = {})
response = conn.get(path, params)
if (response.success?)
Success(response.body)
else
Failure("Basic Error at Repo: #{response.status} #{response.body}")
end
end
end
What do you think?
The only difference is that when using the repo I have to query the result, but the error handling (which is hard for unreliable http-services) is nicer.
Thanks!
Yeah, I generally use monads everywhere in large applications. This way you can compose fault tolerant pipelines. Based on your example above, one thing that might be of value is extracting and encapsulating your connection object so it can be injected into your Repo#initialize
method. This would also mean you could reuse it in multiple objects based on business logic needs.
Here’s an example of my Ghub::API::Client as found in the Ghub gem. In my case, my Ghub::API::Client
is the low level API connection object (using your terminology above). …but notice how my API client exposes the HTTP methods where each answers back a monad. Basically, the encapsulation is pushed down to a lower level from which to build more complex objects upon.
1 Like
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
Thank you @bkuhlmann @alassek. Great input!
1 Like