What are monadic bind and monadic return for C++23 optional?
Asked Answered
T

3

15

C++23 std::optional is finally getting some very useful additions.

Since my knowledge of FP is very primitive I am wondering what is the syntax for the following two operations(that according to my googling are 2 basic monadic operations):

  1. monadic bind
  2. monadic return

My best guesses are:

monadic bind is transform

monadic return is just C++17 std::optional constructor(8)

Towny answered 6/1, 2022 at 11:6 Comment(1)
Why not read the proposal itself?Trombley
M
16

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.

Mercurialism answered 6/1, 2022 at 12:2 Comment(11)
"Is it the non-existing mbind I've mentioned above? Not quite, but it is quite: .and_then is to mbind what .transform is to transform." I have no idea what you're trying to say hereCroissant
Better now, @Barry?Mercurialism
and_then is the monadic bind operation, so I don't know why it's "not quite"Croissant
I've written it (and_then, the member function) isn't quite the mbind I've written above (the free function), just like the member function transform is not the free function transform. As simple as this, @Barry. If stressing to much on the difference between member and non-member seems uselss, then I'm removing it ritght now.Mercurialism
@Croissant do you know if there is a flatten/join mentioned as missing in this answer? I could ask a separate question, but seems a small detail that it might be nicer if added to this answer, obviously if Enlico agrees.Towny
@NoSenseEtAl, I've added a reasoning why I think it doesn't exist. Don't know how convincing it is.Mercurialism
@Mercurialism thank you... but maybe there is something in ranges code that can do this, but I doubt it... since I doubt ranges code even knows about std::optional, let alone C++23 changes.Towny
@NoSenseEtAl, well, as I wrote, .and_then(std::identity) is the wannabe .flatten(). I doubt anything simpler than this is provided. Otherwise it would be named flatten, or flat, or join (<ranges> and Range-v3 do have join but it's again the one for ranges). However, maybe Barry knows something more.Mercurialism
Btw, I've asked a related question.Mercurialism
Do we need brace after the identity in order for this to work auto whatIsThis = optOfOpt.and_then(std::identity{});Juttajutty
@roi_saumon, yes, sorry.Mercurialism
B
8

Not quite.

In Haskell syntax, bind is of the form m a -> (a -> m b) -> m b, which corresponds to satisfying this concept (for all A, B, F)

template <class Fn, class R, class... Args>
concept invocable_r = std::is_invocable_r_v<R, Fn, Args...>;

template <class Bind, template <class> M, class A, class B, invokable_r<M<B>, A> F>
concept bind = invocable_r<Bind, M<B>, M<A>, F>;

That's and_then (with this bound to the first argument). transform is fmap (with this bound to the second argument), which is the Functor operation (all Monads are Functors).

fmap is of the form (a -> b) -> f a -> f b.

template <class Fmap, template <class> M, class A, class B, invokable_r<B, A> F>
concept fmap = invocable_r<Fmap, M<B>, M<A>, F>;

The difference is in the return type of the function being bound or mapped.

Another example of this distinction is .NET's linq Select vs SelectMany

Another nitpick is that the monad laws discuss expressions, not statements, so you'd have to wrap the constructor in a function.

Backup answered 6/1, 2022 at 11:40 Comment(3)
Could you expand this a little bit? Because I'm missing the point. I know C#, C++ and have looked a little at Haskell. I think the difference between Select and SelectMany is that the latter flattens a returned list-of-lists. And I know what andthen would do as in more like a "Fluent syntax". But I don't fully relate this all.Thirsty
@JHBonarius, the monadic binding is transform followed by flattening. However they are defined for the specific wannabe-monad.Mercurialism
@Thirsty they are applicable to different functions. If you have function<B(A)> f and an optional<A> a, you use a.transform(f) to get an optional<B>. If instead you have a function<optional<B>(A)> f then you probably want a.and_then(f) to get an optional<B>, rather than an optional<optional<B>> that transform would give youBackup
P
3

monadic return is just C++17 std::optional constructor(8)

Yes.

monadic bind is transform

No.

Monadic bind accepts a function that of type T -> std::optional<U>. If you just map/transform with such a function, you will get a std::optional<std::optional<U>>

auto continuation = [](T t) -> std::optional<U> {...};
std::optional<T> opt{...};
std::optional<std::optional<U>> out = opt.transform(continuation);

but with monadic bind (spelled std::optional::and_then) we get just one optional out:

std::optional<U> out = opt.and_then(continuation);

This is what monads are (map then flatten) and why they exist to chain together functions of kind T -> Monad<U>


Edit: I'd argue flat_map is the best name for monadic bind, since mapping then flatting out the nested types is what it actually does. And this is how monads should be taught (with optional/future/range) rather than the ridiculous Haskell types and their stupid do notation. Also see this and this.

Pentane answered 28/6, 2022 at 5:41 Comment(1)
@PaulD haskell uses k rather than c to stand for "kontinuation"Pentane

© 2022 - 2024 — McMap. All rights reserved.