mbind
(it doesn't exist, I'm mimicking Haskell's >>=
)
In C++-like pseudo-code, the monadic binding, let's call it mbind
, should have such a signature:
C<U> mbind(C<T>, std::function<C<U>(T)>);
i.e. it should take a monad C
on some type T
, a function that "pulls the inside of that monad out" and turns into a monad C
on a (not necessarily) different type U
, C<U>
, and gives you back that C<U>
.
transform
(the free function)
The transform
you mention, first of all, is a member function, and it has a signature kind of this
C<U> C<T>::transform(std::function<U(T)>);
but let's rewrite its signature as it would be if it was a free function:
C<U> transform(C<T>, std::function<U(T)>);
so as you see it takes a C<T>
and applies a function from T
to U
right inside the functor C
, thus resulting in a C<U>
.
So there's a difference.
To better understand what the difference is, try passing transform
a C<T>
and a function with signature the one that mbind
expects, std::function<C<U>(T)>
.
What do you get? Remember that transform
applies the function "right inside the functor, without pulling anything out", so you get a C<C<U>>
, i.e. one more functorial layer.
mbind
, instead, with the same two arguments, would have given you a C<U>
.
And how can you go from what transform(x, f)
returns to what mbind(x, f)
returns, i.e. from a C<C<U>>
to a C<U>
? You can just flatten/join/collapse/whatever-you-want-to-name-it the two functorial levels, via what's called join
in Haskell and flatten
in some other language.
Obviously if you can do that (and you can for C = std::optional
), then those "functorial layers" were actually "monadic layers".
So is there an mbind
-like thing?
As suggested in the other answer there's the and_then
member function.
Is and_then
the (non-existing) mbind
I've mentioned above? Yes, with the only difference between them being the same as between transform
member function and transform
free function: one is member and the other is free.(†)
And where's flatten
/join
, by the way?
You might wonder if there's this utility in C++23. I have absolutely no clue, I'm barely aware of what C++20 offers.
However, since the function that makes std::optional
a functor is defined as a member function of std::optional
itself, I strongly believe that if there was monadic binding function for std::optional
, it would be defined as a member function too, and in that case it would be at this page, in the section Monadic operations, together with and_then
/transform
/or_else
. Since there isn't, I tend to assume it doesn't exist.
But some knowledge of Haskell helps me give you an idea of why it might be unnecessary to add it to the standard.
What happens if you do this?
auto optOfOpt = std::make_optional(std::make_optional(3));
auto whatIsThis = optOfOpt.and_then(std::identity{});
Yeah, that's it: calling .and_then(std::identity{})
on a nested optional is equivalent to the wannabe .join()
.
What about other libraries?
Boost.Hana defines concepts for Functor
, Applicative
, Monad
, and many others, giving you a way to implement automatically all the abstractions that leverage those concepts at the cost of giving some minimal definition. For instance, if you define transform_impl
and flatten_impl
in namespace boost::hana
for std::optional
, that enables you to use on it transform
, flatten
, chain
, ap
, and any other operation that requires a monad or less.
The following is working code, for instance (complete example on Compiler Explorer):
auto safeSqrt = [](auto const& x) {
return x > 0 ? std::optional(std::sqrt(x)) : std::nullopt;
};
{
auto opt = chain(std::optional(2), safeSqrt);
std::cout << opt.value_or(-1) << std::endl; // prints sqrt(2)
}
{
auto opt = chain(std::optional(-2), safeSqrt);
std::cout << opt.value_or(-1) << std::endl; // prints -1
}
{
auto opt = chain(std::nullopt, safeSqrt);
static_assert(std::is_same_v<decltype(opt), std::nullopt_t>); // passes
}
(†) Originally I stressed a bit more on and_then
not being mbind
because the former is a member function, the latter is a free function.
The reason why I stressed it is that member functions "belong" to classes (in the sense that you can't have a member function without having the class which it is a member of), so in some way the and_then
we discuss here is totally unrelated to the namesake function that we'd write to make std::vector
a monad, because that would live inside std::vector
.
On the other hand, the non-member transform
and the hypothetical mbind
are free functions, so they exist without any need for any class, and so they look a bit more like interfaces to some general abstractions that types can opt in to (like Haskell's type classes). It's clear that, assuming std::transform
and mbind
were customization point, client code that wants to opt in for some type would have to write a customization for that type, and maybe that could leverage a member function.
Answering to this question made another question pop up in my mind, so I've asked it.