Monads are a great tool in every programming language, they provide a simple way to hide side-effects in your code, and in languages that allow even the slightest amount of pattern matching, they enable "Railroad Oriented" programming.
Thankfully, Raku has given
which enables us to perform simple pattern matching, and with it's dependent-type system we can easily define our own Monads. I wrote Monad::Result a month or two ago, and I have used it in almost all of my Raku projects since.
Simply install Monad::Result
with zef:
zef install Monad-Result
Monad::Result
is an extremely simple package that is designed to allow programmers to easily hide exceptions within a container, forcing the caller to evaluate both possible scenarios of the call. This is particularly useful when dealing with IO, Network calls, Databases, Asynchronous callbacks, etc. Basically anything with a well-defined side-effect. If you don't know what a side-effect is, it's basically the "sad-pass" of an operation. So in the case of an HTTP request, it may fail due to a number of reasons, it's failure would be it's side-effect. What's nice about using a Monad to represent this, is we can hide all the ugliness behind an easily consumed interface, whereas with exceptions it becomes quite cumbersome.
Abstracting away exceptions
The most common way I find myself using Monad::Result
is with exceptions. Since I can't specify the exceptions in the type-signature, or return type of a function, I find they often go uncaught and lead to some gross bugs. But, with Monad::Result
as the return type, the developer calling the code can infer "hey, this code might fail".
Here is a simple wrapper around DBIish
I typically use to hide exceptions from the caller:
use DBIish;
use Monad::Result :subs;
my $db = DBIish.connect('SQLite', :database<foo.db>);
sub exec-sql(Str:D $sql, *@args --> Monad::Result:D) is export {
CATCH {
default {
return error($_);
}
}
my $stmt = db.prepare($sql);
ok($stmt.execute(|@args).allrows(:array-of-hash));
}
Let's break down what's going on inside the exec-sql
sub-routine.
Since DBIish will fire an exception if something goes wrong, we catch it in the CATCH
, it will catch any exception and return it as a Monad::Result{+Monad::Result::Error}
basically a result of error, and obviously, if it succeeds, then the sub-routing will return a Monad::Result{+Monad::Result::Ok}
, meaning it succeeded.
So if we wanted to use our new exec-sql
sub-routing downstream we'll know explicitly that this call can, and will fail. So, we can do some pattern-matching with given
to make sure our code is super safe!
Here is how you typically see a Monad::Result
consumed:
given exec-sql('SELECT * FROM users') {
when Monad::Result::Ok:D {
my @users = $_.value; # safe to do so because we know we're Ok!
say "Got all users! @users";
}
when Monad::Result::Error:D {
my $exception = $_.error;
warn "Failed to get all users, oh no! Had this error $exception";
}
}
We simply match against the two possibilities for a Monad::Result
, and our code remains completely safe.
Bind and Map
Now you're probably wondering "How the heck is this practical, what if I have to combine a hundred side-effect calls? This will become a mess!" And you'd be right, especially if Raku was a bad language that didn't allow user defined operators. (Just joking Go isn't bad, but Raku is simply better :^). Because of this, we can use the >>=:
(Bind) and >>=?
(Map) operators respectfully to manage our Monad.
These operators are super simple, all they do is take a Monad::Result
on the left and a Callable
on the right. Then, if the Monad::Result
on the left is Ok, it will call the Callable
with underlying value of the result. If the result isn't Ok, we just pass the error along. This allows for beautifully chained side-effects without the possibility of an exception blowing up on you (assuming you only call code that can't cause an exception). If you want to use another exception causing routing within a Bind or Map operation you'll have to write a wrapper that returns a Monad::Result
instead.
exec-sql('SELECT * FROM users') >>=: { say @^a; ok(@^a.map(*.<name>)) };
This code will select all users from our database, if it succeeds we'll print to the console all of the users, and map the list to just their name.
We could also use the >>=?
(Map) operation to avoid having to wrap our return with ok
.
exec-sql('SELECT * FROM users') >>=? { say @^a; @^a.map(*.<name>) };
Now, if you're not really a Functional Programmer, these operators might be a little scary. Instead, if you wish, you can use the OO versions like so:
exec-sql('SELECT * FROM users').bind({ say @^a; ok(@^a.map(*.<name>)) });
and
exec-sql('SELECT * FROM users').map({ say @^a; @^a.map(*.<name>) });
Personally I like to use the OO version if I have to Bind or Map more than 3 times in a row.
Monads in the wild
Here are some snippets of some production code that uses Monad::Result
Find-one SQL
sub find-one($query, *@args) {
exec-sql($query, |@args) >>=: -> @rows { @rows.elems == 1 ?? ok(@rows[0]) !! error(@rows) };
}
This is a simple wrapper around the earlier exec-sql
routine that binds to it's result and if it isn't exactly one element it will return an Error result.
HTTP chaining
Note: This code isn't in production yet, but it's a part of a large project I'm working on :)
Another cool use for Monad::Result
is for HTTP requests, sometimes we need to chain them together, here is a simple example of chaining 3 HTTP requests that rely on each-other:
auth-client.get-user-roles($id)
.bind({ 'ADMIN' (elem) @^a ?? user-client.get-user($id) !! error('User is not an admin') })
.bind({ dashboard-client.find-dashboard-by-user-id(@^a<id>) });
In this example, auth-client
, user-client
, and dashboard-client
, are all wrappers around Cro::HTTP::Client
that perform HTTP requests to different services. All of the end-points are abstracted by a function and return a Result::Monad
, allowing the developers consuming them to simply chain them.
Conclusion
Monad::Result
allows us to very concisely and elegantly deal with real-world side-effects. You can use them to simply chain side-effect reliant code without having to write a huge CATCH
block, and they also give the developer consuming our code the chance to realize that the code may return an Error.
Monad::Result is not the only monad in the Raku ecosystem, there is also Definitely by Kay Rhodes (aka Masukomi) which provides a very nice "optional" type, that can fill a similar role to Monad::Result
.
Thanks for reading, Raku Rocks!
Top comments (0)