Post

Implement monadic and_then for std::variant

In a previous post we have explored the monadic operations transform and and_then, implemented for std::optional and std::expected in C++23. The main advantage of using these operations is that they make code easier to parse and reduce the use of intermidiate variables.

In this post we will focus on an implementation of and_then for std::variant. The first question to answer is what exactly should this operation do? We can adapt the definition used previously for this new type.

  • and_then accepts an input std::variant<Q, R> and a function for each variant type defined as:
1
2
std::variant<S, T> f(Q q);
std::variant<S, T> f(R r);

and returns a std::variant<S, T> being the result of f(q) or f(r).

To better follow the implementation and the current limitations of std::variant API for this use case let’s consider a toy example. Let’s assume we have a std::variant<T, std::complex<T>> and we want to convert this to a std::variant<T, std::tuple<T, T>>, following this two-steps algorithm:

  • If the starting variant holds a T we will first transform it to a std::complex<T,T> where the real and imaginary parts are equal to the value in the starting variant. Else if the variant holds a std::complex<T,T> we will first transform it to T returning the norm of the complex number. We will call this the Cross operation.

  • In the second step, if the new variant holds a T we will just return it. Otherwise, if it holds a std::complex<T,T> we will return a std::tuple<T,T> containing the real and imaginary part of the starting complex number. We will call this the MaybeConvertoperation.

We could implement this algorithm simply using std::visit as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <variant>
#include <complex>
#include <tuple>

template <typename T>
using variant_1 =  std::variant<T, std::complex<T>>;

template <typename T>
using variant_2 = std::variant<T, std::tuple<T, T>>;


struct CrossVisitor {
    
    template<typename T>
	constexpr variant_1<T> operator()(const T i) {
		return std::complex<T>{i, i};
	}

    template<typename T>
    constexpr variant_1<T> operator()(const std::complex<T> c) {
		return norm(c);
	}
};



struct MaybeConvertVisitor {

	template<typename T>
	constexpr variant_2<T> operator()(const T f) {
		return T{f};
	}

    template<typename T>
	constexpr variant_2<T> operator()(const std::complex<T> c) {
		return std::tuple<T, T>{std::real(c), std::imag(c)};
	}

};


int main() {

	constexpr variant_1<float> v = float{3};
    constexpr auto converted = std::visit(MaybeConvertVisitor{}, std::visit(CrossVisitor{}, v)); // (1)

    static_assert(std::get<0>(std::get<std::tuple<float, float>>(converted)) == float{3});
    static_assert(std::get<1>(std::get<std::tuple<float, float>>(converted)) == float{3});
}

In the snippet above we have leveraged the ability of std::visit to work with functors, and the result is correct however this solution is not optimal (if we had and_then on std::variant) for at least two reasons:

  • When reading (1) the order of operations is mixed, we first read MaybeConvertedVisitor and later CrossVisitor.
  • Line (1) is cluttered by noise, namely std::visit.

If we had the possibility to add member functions to std::variant we could implement and_then, but we will choose a different and less intrusive option. We will implement this operation using the | operator. This feels like a natural choice since the pipe operator is already in use in std::ranges where it conveys a similar meaning, and also is the easiest option to modify this STL type. A simple implementation looks like this

1
2
3
4
5
6
7
8
9
10
11
12
namespace monadic_variant{
    template<typename... Ts, typename Visitor>
    constexpr auto operator| (const std::variant<Ts...>& v, Visitor&& callable) {
    	return std::visit(std::forward<Visitor>(callable), v);
    }

    template<typename... Ts, typename Visitor>
    constexpr auto operator| (std::variant<Ts...>&& v, Visitor&& callable) {
        return std::visit(std::forward<Visitor>(callable), v);
    }
}

The monadic_variant namespace implements and_then using the pipe operator. Its implementation is very simple and it consists of a template variadic | operator, so it can bind to any variant, and passes it to a std::visit together with a callable (functor, lambda etc…), passed in as a universal reference. We provide two overloads so that we can call an optimized version in the case we have a r-value or an l-value std::variant.

Using this new operator we can change our main function to the following

1
2
3
4
5
6
7
8
9
int main() {
	constexpr variant_1<float> v = float{3};
    {
        using namespace monadic_variant; // (1)   
        constexpr auto converted = v | CrossVisitor{} | MaybeConvertVisitor{}; // (2)
        static_assert(std::get<0>(std::get<std::tuple<float, float>>(converted)) == float{3});
        static_assert(std::get<1>(std::get<std::tuple<float, float>>(converted)) == float{3});
    }
}

The main differences with our previous main are in (1) and (2). In (1) we use the monadic_namespace namespace so that the compiler can resolve the | operator. Thank to this operator used in (2) we can now leverage a fluent and more natural API. This is closer to functional style programming for std::variant.

In conclusion we have implented one of the monadic operations, and_then, defined in C++23 in terms of pipe operator for std::variant using std::visit, leading to more readable code.

This post is licensed under CC BY 4.0 by the author.