Monads, Promises, and Fluent Programming
I read Stepan Parunashvili Inventing Monads, in which he starts with some small, simple functions and quickly runs into problems in their use. He mutates them until he ends up with monads, a hot feature I mostly associate with Haskell. These compose separate actions to get something that’s safe and predictable.
I wanted to redo this is Perl so I could make some other points. Stepan contrives a situation to get to a particular endpoint, which is what us tech writers do. He wants to show something about Monads and that’s why he follows the progression he does. He’s not trying to suggest a particular way of programming.
Chaining operations
I’ll mostly follow Stepan’s progression, which starts with a set of functions that rely on the others:
You always have the $id
value, and you build up from that to get the profile and to get the display picture:
The problem is that this only works if everything is perfect. There’s a user with that ID, that user has a profile, and that profile has a display picture. First, I’ll set aside that I’d do all of this in a different way where I wouldn’t have to know the low-level details, such as needing the profile to get the picture. It’s an example and I’ll stick with that as presented.
As an intermediate step, Stepan invents the Chainer
, a composer that can recognize when the previous step has failed. The chain starts with the info at the bottom of the process and then goes through the steps to get to the final desired value:
If there’s a value, it uses that value as the argument to the next step. (For Perl people, the =>
in his code is a fancy way to define a lambda, not create a pair. It’s SIGNATURE => CODE
.).
In that code, each step is a new Chainer
object, either the new one with a new value or the current one. Stepan modifies that for the case where one of the functions already returns a chained thingy. In that case (in merge
) you don’t make a new Chainer
:
Since he always returns a Chainer
thingy, I don’t see how he gets around a failure. You get one of the thingys, maybe the initial one, and then call when
on it. The value that the Chainer
stored was for the previous step but now he tries to use it for the next step. I could be missing something in his setup, but he never mentions how things work when any step fails.
Not only that, I shouldn’t need the extra merge
method. I can look at the value to see if it’s a Chainer
sort of thing (in this case, using the new isa operator from v5.32). This might not be allowed:
From there, you can follow [Stepan’s post]((https://stopa.io/post/247) to learn more about how this resembles monads. I want to look at the particular problem and a few techniques for dealing with it.
Promises
So, lets try something else. How about Promises? I only want to go to the next step if the previous step works? Stepan mentions that the Chainer
sidesteps callback hell, but so do Promises:
Mojo::Promise is a Perl implementation of A+ Promises. I’ve wrote Higher Order Promises for the 2018 Mojo Advent Calendar, and solved some exercises using Raku’s Promises.
Here’s what I think the Promise version of Chainer
. Each step leads to the next then
:
This outputs the message for that step:
That’s fine. The chain happens. But what if I reject the $promise
?
Now I get an error because I don’t have a handler:
I can handle that anywhere I like, but a catch
at the end works:
But, a catch
is just a then
with only a reject
branch:
Now expand that into something real. Create a Promise and resolve it with the user ID I want to find. That’s primes the pump.
After that, use a series of then
s that each do the next step in the process. Each then
only handles the resolve
branch. If the function call works (returns a defined value in this example), I return that defined value. That value becomes the argument for the subroutines in the next then
. If I make it all the way to the final then
with a resolved Promise, I assign to final value (the picture) to $picture
and output a message.
If something doesn’t work, I return a rejected Promise that knows why it failed. Since nothing else but the final then
handles the rejected branch, the interstitial then
s are effectively skipped and I can look in the argument list for the reason it failed:
Here are some runs, where I neglect to supply a user ID, give one I know doesn’t exist, and so on so I get all the responses:
But, that code is pretty ugly. One of the major problems (among others), is that I hard-coded the chain. That’s easy enough to fix—I’ll construct all the middle then
s from a table instead. Rather than check with defined
, I’ll add a new step to verify the input.
Method chaining
There’s a trick I like a bit too much, but it’s fun enough to justify sometimes. Sometimes this sort of thing is called fluent programming, but I’m going to stick with “method chaining”. The trick is handling errors in the middle of the chain.
I want to write something like this, where I have a chain of methods one after the other. This isn’t a good design for this problem, but it’s fine for showing the method chaining idea. Each method returns an object. It could be the same object or different objects. I don’t have to know too much about that:
So, how do I handle errors? Here’s the trick. If a method fails, I’ll return a null object that responds to any method name by returning itself. That soaks up the rest of the method chain without an error. When you want to know if the whole thing worked, you look at what you got back. This is the same object type technique I used in No ifs, ands, or buts, The Null Mull, and the StackOverflow answer How do I handle errors in methods chains in Perl?:
Here’s the null class. The new
creates it and the AUTOLOAD
handles any method not defined by returning the null object again. I handle DESTROY
, a special Perl finalizer method to break an infinite loop:
To handle everything else, stuff moves into a class. Each method either succeeds or returns a null object:
There are various ways that I can reduce this to create new classes on the fly with just the operations I want to do, but that’s some tricky code I don’t want to explain here.