Dry-Monads: Extend `Result`?

Hi there, and thanks for all the fantastic work that we’re getting to use.

We’re in the process of writing a library to access popular shipping providers - think ActiveShipping, but somewhat more modern and not archived. Here’s the gem if you’re interested (no need to read the gem, the question is entirely in this post):

We’re making heavy use of the Result monad to wrap successful or unsuccesful API calls.

The API goes somewhat like this:

>>> ups_service.find_rates(shipment)
Success([rate1, rate2, rate3])

Now, for both Success and Failure, my colleagues are asking for debugging information because shipping APIs are complex and sometimes unpredictable, and it takes a lot of inspection to find out what went wrong. They would, specifically, like the shipment object that was passed in as well as the request that was generated as well as the response obtained from the provider.

Currently, we have another object around the rates array that does this, so that we can do this:

>>> result = ups_service.find_rates(shipment)
Success(rates_result)
>>> result.value!.rates
[rate1, rate2, ...]
>>> result.value!.original_request
my_request

However, I believe it would be more ergonomic and nicer to be able to access the debugging info directly on the Result object:

>>> result = ups_service.find_rates(shipment)
Success(rates_result)
>>> result.original_request
my_request

Is this possible? I’ve tried subclassing Result, Success and Failure, but that seems to be pretty hard as all the syntactic sugar (constructor functions etc) need to be adapted as well, as the initialize signature changes. I’m also not sure whether specialized Result monad would play well in the rest of the Ecosystem. I was thinking of adding to_dry_result to ameliorate this.

How do you go about these kinds of things? There’s one value that is really the thing you want, and a bunch of less important info that should go in the result as well.

What I’ve seen in the docs is using Arrays or Hashes for the value that’s passed into the monad. That, however, is similar to the result container presented in the first example.

Caveat also: I’m not new to Ruby, but I am pretty new to functional programming and the Dry ecosystem. It’s entirely possible there’s a much better way of achieving what I want: An ergonomic API interface that lets library users get quickly at what they need.

Thank you for reading, and again: Thank you, I love your work!

Okay, after sleeping over this and discussing with my colleagues, we agreed to use the pattern we currently have - just slightly modified, calling the instance variable that contains the rates data.

>>> result = ups_service.find_rates(shipment)
Success(rates_result)
>>> result.value!.data
[rate1, rate2, ...]
>>> result.value!.original_request
my_request

The biggest advantage of this pattern that we would forgo with the solution proposed in the original post is that with this, we would actually have the debugging information available inside any bind or fmap blocks - plus the implementation is MUCH easier.

Yes, that’s the approach I would recommend. We had a similar problem with dry-schema’s Result object and its to_monad method. The question was whether you want result.to_monad to be Failure(key: 1, ...) or Failure(Dry::Schema::Result::Failure(key: 1)). We ended up with the second option since the specific result carries more info about input data. Practically speaking, it’s not important, since all dry-monads wrappers are automatically removed by do notation.

What comes to extensibility, the right way is to build your own ADT (stands for algebraic data type). At the moment, we don’t offer a way/API/public interface for this since it would a be bit clumsy in ruby. It’s also not an everyday need, Result and Maybe work for most cases. That said, this may be added in future.

1 Like