Monadic operations in C++23
If you are a C++ developer, and you have not been living under a rock, you certainly know that C++23 is finalized! One of my favourite addition is monadic operations.
Before diving into the details, let’s take a step back and try to define a monad. I will give a very simple (maybe incomplete) definition, that comes from a software developer perspective. For the purpose of this discussion a monad M that accepts a type T provides a set of operations:
- A constructor or factory function that accepts any type
T
and “lifts” it to the monadic type. - An operation
transform
that takes as input a monadM<T>
and a function1
S f(T t);
and returns a
M<S>
, withS
being the result off(t)
, ifM<T>
exists, or a default monadic value ifM<T>
does not exist. - An operation
and_then
that takes as as input a monadM<T>
and a function1
M<S> f(T t);
and returns a
M<S>
, withM<S>
being the result off(t)
, ifM<T>
exists, or a default monadic value ifM<T>
does not exist. - An operation
or_else
that takes as as input a monadM<T>
and a function, and returnsM<T>
if it exists or executes the function.
If you are wondering what is the difference between transform
and and_then
I got you covered! Simply put, if you where to apply transform
to a function
1
M<S> f(T t);
you could get the output M<M<S>>
so and_then
simply flattens the result of applying f
to t
.
By this definition of monad in C++23 optional
became a monadic type! To understand why we care let’s look at a simple example.
Let’s say I want to compute the square root of a number, later I will add 42 to this number. Using c++17 optional
an implementation might look like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cmath>
#include <optional>
#include <iostream>
std::optional<float> my_sqrt(float x){
return x > 0 ? std::make_optional(sqrtf(x)) : std::nullopt;
}
float my_add_42(float x){
return x + 42;
}
int main(){
const std::optional<float> x = 144;
std::optional<float> out = std::nullopt; /* (1) */
auto sqrt_x = my_sqrt(*x);
if (sqrt_x.has_value()){ /* (2) */
out = my_add_42(*sqrt_x);
}
std::cout << out.value_or(-1) << std::endl;
}
In the snippet, if x is a negative numer or std::nullopt
, I will get -1
, otherwise the value of the operation will be displayed. We deal with the condition that my_sqrt
could return an empty optional by checking if the result has a value (2). This is not ideal because breaks the logic flow. Also I am forced to first initialize the out
variable (1) and then assign a value to it. Using the power of monadic operations in c++23 we can craft a cleaner solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cmath>
#include <optional>
#include <iostream>
std::optional<float> my_sqrt(float x){
return x > 0 ? std::make_optional(sqrtf(x)) : std::nullopt;
}
float my_add_42(float x){
return x + 42;
}
int main(){
const std::optional<float> x = 44;
const std::optional<float> out =
x.and_then(my_sqrt)
.transform(my_add_42)
.or_else( [](){return std::make_optional<float>(-1); });
std::cout << *out << std::endl;
}
This code is functionally equivalent to the previous snippet but it has three main advantages:
- Reduced use of if statements.
- output variable is assigned in only one statement, so it can be const.
- Easy chaining of operations leading to fluent API.
optional
is not the only type to implement monadic operations. A similar interface is implemented by expected
, that I will describe in a future post.