DEV Community

Cover image for Practical Monads with Raku and Monad::Result
Rawley Fowler
Rawley Fowler

Posted on

Practical Monads with Raku and Monad::Result

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
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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>)) };
Enter fullscreen mode Exit fullscreen mode

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>) };
Enter fullscreen mode Exit fullscreen mode

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>)) });
Enter fullscreen mode Exit fullscreen mode

and

exec-sql('SELECT * FROM users').map({ say @^a; @^a.map(*.<name>) });
Enter fullscreen mode Exit fullscreen mode

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) };
}
Enter fullscreen mode Exit fullscreen mode

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>) });
Enter fullscreen mode Exit fullscreen mode

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)