The initial example code is broken: the functions don’t test whether the optional has a value, and write to it regardless.
In these examples it won’t be a noticeable problem because setting a field just writes into a byte inside the optional, but it’s UB regardless, and in a more complex example could do bad stuff. Plus it doesn’t demonstrate how you’d actually implement these functions.
There is an emptiness check before they’re called though. As written I don’t think there’s UB, but the implicit contract for those functions does imply that it must be populated.
Unfortunately, accurately describing monads in general is infamously difficult. Difficult enough that I am not going to try, because I don’t think I can do it well.
I promise I am being sincere when I say that the best way to really understand what is going on (and to get the full benefit of the concept when writing code), try working with monads in a language that provides fully support for them. For me that language was Haskell. I don’t write it very often any more, but learning it changed the way that I think about programming forever. I miss this type of support daily when writing C++.
Specifically, while these new operations do allow use of std::optional as a pseudo-monad, they do not unfortunately come close to allowing C++ to expose any kind of arbitrary monad (say, via a concept). It’s a drop in the bucket of what makes monads as useful as they could be.
That being said, this new functionality will be very useful. It’s just not quite enough IMO.
Lots of things are “mappable.” Like, an optional type – you can run a function on an optional type and turn that into a different optional type. You can “map” a vec into another vec. You can map futures/deferreds/promises into other futures/deferreds/promises. This is C++’s transform method.
Lots of things are also “thenable.” Like a future – you can execute the future, and run a function on the result that returns a new future. For optionals, that means running a function on the value inside the optional that itself returns an optional. This is C++’s and_then method.
That’s all a “monad” is. When someone says “optional<T> is a monad” they mean “optional<T> is thenable.” It’s a pattern that happens to crop up in a lot of places for a lot of types, and recognizing that pattern is fun and useful.
or_else is something different than a monad. “Monads” are not “or-able.” “Alternatives” are “or-able.” Options happen to be monads and alternatives, but conflating the two feels confusing to me.
All of these methods happen to chain nicely – they return a type of the same class that you invoke them on. I think that that’s what you’re responding to. But that’s not “what a monad is” – lots of methods chain that are not the particular and_then method that makes something “monadic.”
If you were to explain monads to a C programmer, you would say that monads are basically tagged unions and functions that operate on those tagged unions. With this explanation, you cover almost all real world functional programming applications of monads.
For C++ programmers this is new, for those who moved on to Rust this is daily business for many years already. I wonder how long it takes the C++ committee to adopt the rest of the Rust interface. Maybe another 5-10 years, with compilers supporting the new features trickling down to the industry in 15 years? While it is encouraging that C++ is being modernized, the fact that the real innovation happens in Rust cannot be overlooked (and yes, I am aware that Rust’s Option is inspired by Haskell’s Maybe type [1] and other, older functional programming concepts, but the difference here is that Rust does not insist on functional purity and makes these concepts easy to understand and usable in low-level contexts like OS and real-time programs).
It is not! It doesn’t have niche optimisations and layout guarantees that could make it zero-cost without size overhead.
But most importantly Rust’s Option is safe from easy-to-hit UB, but std::optional isn’t. C++ has chosen dereference to be quietly UB instead of guaranteeing abort.
Even this article has potential for UB:
if (!p->has_pepperoni)
p->has_pepperoni = true;
p-> is UB if the optional doesn’t have a value.
I find this design decision flabbergasting — I know it’s for speed and consistency with all the other unsafe dereferences, but modern C++ was supposed to stop doing this reckless stuff. They’ve been so close to creating a safe abstraction, and chose to make yet another fancy nullable pointer instead.
I fully agree with you, but I assumed that easy-to-hit UB is already implied when we are talking about C++, which is why I didn’t mention it. However, I have to admit, while I was skimming the article, I didn’t notice the potential UB in the examples that was pointed out by @snej and you.
It’s not surprising that a language created 10 years ago would adopt these features more quickly than one created 40 years ago. As for the features themselves, they’re hardly novel, as you point out. And they can be implemented in a couple of lines of code apiece in C++ (I’ve done it.)
Rust 1.0 was released in 2015 [1] already having the and_then, or_else and map interface [2].
std::optional was introduced in C++17 [3].
6 years pass (???)
std::optional is extended by the three most obvious, simple functions imaginable
I don’t know the full story of why it took so long. And I’m not blaming any individual here - I know how hard it is to negotiate interfaces and standards in a committee. But it is clear that the C++ committee is dysfunctional in many ways. It seems to me that the language, more than ever, lacks a clear vision and that the committee has inscrutable priorities.
The initial example code is broken: the functions don’t test whether the optional has a value, and write to it regardless.
In these examples it won’t be a noticeable problem because setting a field just writes into a byte inside the optional, but it’s UB regardless, and in a more complex example could do bad stuff. Plus it doesn’t demonstrate how you’d actually implement these functions.
There is an emptiness check before they’re called though. As written I don’t think there’s UB, but the implicit contract for those functions does imply that it must be populated.
So all this “monadic” talk is just returning a reference to *this?
No, it’s much more than that.
Unfortunately, accurately describing monads in general is infamously difficult. Difficult enough that I am not going to try, because I don’t think I can do it well.
I promise I am being sincere when I say that the best way to really understand what is going on (and to get the full benefit of the concept when writing code), try working with monads in a language that provides fully support for them. For me that language was Haskell. I don’t write it very often any more, but learning it changed the way that I think about programming forever. I miss this type of support daily when writing C++.
Specifically, while these new operations do allow use of
std::optional
as a pseudo-monad, they do not unfortunately come close to allowing C++ to expose any kind of arbitrary monad (say, via a concept). It’s a drop in the bucket of what makes monads as useful as they could be.That being said, this new functionality will be very useful. It’s just not quite enough IMO.
Describing Monads in general is hard but jerf is very good at explaining things!
Functors and Monads For People Who Have Read Too Many “Tutorials”: https://www.jerf.org/iri/post/2958/
Lots of things are “mappable.” Like, an optional type – you can run a function on an optional type and turn that into a different optional type. You can “map” a vec into another vec. You can map futures/deferreds/promises into other futures/deferreds/promises. This is C++’s
transform
method.Lots of things are also “thenable.” Like a future – you can execute the future, and run a function on the result that returns a new future. For optionals, that means running a function on the value inside the optional that itself returns an optional. This is C++’s
and_then
method.That’s all a “monad” is. When someone says “
optional<T>
is a monad” they mean “optional<T>
is thenable.” It’s a pattern that happens to crop up in a lot of places for a lot of types, and recognizing that pattern is fun and useful.or_else
is something different than a monad. “Monads” are not “or-able.” “Alternatives” are “or-able.” Options happen to be monads and alternatives, but conflating the two feels confusing to me.All of these methods happen to chain nicely – they return a type of the same class that you invoke them on. I think that that’s what you’re responding to. But that’s not “what a monad is” – lots of methods chain that are not the particular
and_then
method that makes something “monadic.”If you were to explain monads to a C programmer, you would say that monads are basically tagged unions and functions that operate on those tagged unions. With this explanation, you cover almost all real world functional programming applications of monads.
This is basically a carbon copy of Rust’s Option.
and_then
(Rust) →and_then
(C++)or_else
(Rust) →or_else
(C++)map
(Rust) →transform
(C++)For C++ programmers this is new, for those who moved on to Rust this is daily business for many years already. I wonder how long it takes the C++ committee to adopt the rest of the Rust interface. Maybe another 5-10 years, with compilers supporting the new features trickling down to the industry in 15 years? While it is encouraging that C++ is being modernized, the fact that the real innovation happens in Rust cannot be overlooked (and yes, I am aware that Rust’s
Option
is inspired by Haskell’sMaybe
type [1] and other, older functional programming concepts, but the difference here is that Rust does not insist on functional purity and makes these concepts easy to understand and usable in low-level contexts like OS and real-time programs).[1] https://notes.iveselov.info/programming/cheatsheet-rust-option-vs-haskell-maybe
It is not! It doesn’t have niche optimisations and layout guarantees that could make it zero-cost without size overhead.
But most importantly Rust’s
Option
is safe from easy-to-hit UB, butstd::optional
isn’t. C++ has chosen dereference to be quietly UB instead of guaranteeing abort.Even this article has potential for UB:
p->
is UB if the optional doesn’t have a value.I find this design decision flabbergasting — I know it’s for speed and consistency with all the other unsafe dereferences, but modern C++ was supposed to stop doing this reckless stuff. They’ve been so close to creating a safe abstraction, and chose to make yet another fancy nullable pointer instead.
I fully agree with you, but I assumed that easy-to-hit UB is already implied when we are talking about C++, which is why I didn’t mention it. However, I have to admit, while I was skimming the article, I didn’t notice the potential UB in the examples that was pointed out by @snej and you.
It’s not surprising that a language created 10 years ago would adopt these features more quickly than one created 40 years ago. As for the features themselves, they’re hardly novel, as you point out. And they can be implemented in a couple of lines of code apiece in C++ (I’ve done it.)
and_then
,or_else
andmap
interface [2].std::optional
was introduced in C++17 [3].std::optional
is extended by the three most obvious, simple functions imaginableI don’t know the full story of why it took so long. And I’m not blaming any individual here - I know how hard it is to negotiate interfaces and standards in a committee. But it is clear that the C++ committee is dysfunctional in many ways. It seems to me that the language, more than ever, lacks a clear vision and that the committee has inscrutable priorities.
[1] https://blog.rust-lang.org/2015/05/15/Rust-1.0.html
[2] https://doc.rust-lang.org/src/core/option.rs.html#1432
[3] https://en.cppreference.com/w/cpp/utility/optional