Jonathan Boccara's blog

The Optional Monad In C++, Without the Ugly Stuff

Published July 7, 2017 - 3 Comments

The last post on Fluent C++ showed how several functions that could fail could be chained together by encapsulating the checks into an optional monad, so that the calling code doesn’t have to worry about checking each function call.

That post stirred up a lot of reactions. Some people found it interesting and inspiring. Other people deemed that the resulting C++ code was way too complex. And some other people were enthusiastic with the fact that it was a different approach from what we’re used to seeing.

I think I’m in the three categories at the same time.

In particular, I recognize that the resulting code is kind of scary, especially if you don’t spend your week-ends doing functional programming. In all cases, one of my goals was to introduce the subject gradually, and at least I hope I succeeded in doing that.

Now I want to show you how to encapsulate the optional monad in a different, more sophisticated way (which is why I recommend you start by reading the previous post to get the full story), but that totally relieves the client code from the complex stuff.

I owe this step towards expressiveness to Jacek Galowicz. He suggested to me what ended up as the core idea of the first section of this post, and that idea showed me the direction to much more, which I’ll expose in future posts. Jacek hosts a great blog and just published a very promising book, you should check them out both.

Functions with arguments that could fail

Let’s take an API that has several functions:

int f1(int a);
int f2(int b, int c);
int f3(int d);
int f4(int e);

To use this API we chain calls to its functions, by feeding an initial value to f1. For example:

f4( f4( f3( f2( f1(42), f1(55) ) ) ) )

All good so far. Now what if we’re not sure their are initial values? Maybe their calculation failed for some reason.

For this reason, we choose to model input values with optionals. Now, how can we feed optionals to this API without changing it, and without checking for failures at every call?

For this we wrap the error checking into a function, that can be generated on the top of a function from our interface (yeah, read that sentence twice):

template <typename R, typename ... P>
auto make_failable(R (*f)(P ... ps))
{
    return [f](std::optional<P> ... xs) -> std::optional<R>
    {
        if ((xs && ...)) {
            return {f(*(xs)...)};
        } else {
            return {};
        }
    };
}

make_failable takes a function f (for example one in our API), and returns a new function, that essentially forwards calls to f but manipulates optionals and checks for failure. The variadic templates allow to wrap functions with any number of arguments, and the xs && ... is a fold expression, appearing in C++17. Note that this particular implementation accepts functions, but not more general callable objects. And also note that, as of C++17, std::optional doesn’t accept references (boost::optional does, and all this constitutes the topic of another post).

So, we wrap the functions of the API the following way:

auto failable_f1 = make_failable(f1);
auto failable_f2 = make_failable(f2);
auto failable_f3 = make_failable(f3);
auto failable_f4 = make_failable(f4);

And this is it! We can use these functions supporting optionals instead of the original ones, and they’ll do just the right thing. For example, if x and y are optional<int>s, then the following expression:

failable_f4( failable_f4( failable_f3( failable_f2( failable_f1(x), failable_f1(y) ) ) ) )

returns what the original calls of the API would have returned, wrapped into an optional if both x and y do contain a value, and std::nullopt otherwise. And this calling code doesn’t have to worry about checking the failures at every step of the call chain.

How cool is that??

Functions that could themselves fail

Now let’s add to the requirements that, on the top of the support for failed arguments, we want to allow some functions of the API to fail themselves, even if they receive a correct argument. A failure has to come from somewhere, right?

So let’s modify the API so that, say, f3 can introduce a failure:

int f1(int a);
int f2(int b, int c);
std::optional<int> f3(int d);
int f4(int e);

And we would still like to chain up the functions calls and feed optionals to them, without worrying about checking for failures. Except that an optional can now originate from right in the middle of the call chain.

Let’s reuse the same idea of wrapping an API function into one that checks the error. But this time we don’t have to wrap the value coming out of the API function into an optional, since it’s already one.

This gives the following wrapper:

template <typename R, typename ... P>
auto make_failable(std::optional<R> (*f)(P ... ps))
{
    return [f](std::optional<P> ... xs) -> std::optional<R>
    {
        if ((xs && ...)) {
            return f(*(xs)...);
        } else {
            return {};
        }
    };
}

This overload of make_failable looks similar to the other one, except for 2 things:

  • the argument of make_failable returns an optional,
  • the return statement in the if branch directly returns what f returns, without wrapping it into an optional – it’s already one.

And now with the same wrapping style we get:

auto failable_f1 = make_failable(f1);
auto failable_f2 = make_failable(f2);
auto failable_f3 = make_failable(f3); <= this one can introduce a new failure
auto failable_f4 = make_failable(f4);

And again:

failable_f4( failable_f4( failable_f3( failable_f2( failable_f1(x), failable_f1(y) ) ) ) )

that returns an optional with the right value inside if all went well, or std::nullopt if any function or parameter failed at some stage.

Did it go too fast?

Did this somehow look magical? If so then don’t worry, that’s how this kind of programming (functional) feels when you’re not used to seeing it. If you’re not sure you got it, I’d recommend you read the full story starting in the previous post and with a simpler implementation, and calmly work your way up to the whole solution presented here. And if this is still unclear, I’m always here to help.

Next week we’ll do a similar work with vector. Like with optional we’ll start by a naive implementation to get our feet wet with the technique, and then move on to a sophisticated one involving advanced components amongst our friends the ranges. Exciting week ahead, right?

Until then, have a lovely functional week-end.

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed