How to Use Monadic Operations for `std:optional` in C++23
cppstories.com
cppstories.com
If I didn't learn monads with haskell before, I would think that monads is some useless boilerplate abstraction
you are welcome.
[support for all monads, or, you know, actual monad semantics left as an exercise to the reader]
It always seems to best suit languages that have built in type/pattern matching and you actually design your functions to behave differently given a type vs obscuring what is nested if/else statements? (I know Rust can pattern match.)
I think there is some readability improvements when you're able to turn `if & if & if x | else | else | y` into `if & if & if x | y` by letting the empty type "fall out", but as soon as you have something like `if(if(x | y) | z)` the fall out nature has less effect.
They can be mis/overused, but I think the declarative aspect and ability to split processing into clear steps makes them very helpful. They also allow "ignoring" the negative side or easily shunting values there, which may require repeated conditional checking (and obscure the actual processing) in a language without monadic operations.
They're also very compositional, so you don't have to update every API e.g. Java needs both a `Map#get` and a `Map#getOrDefault`, and that still doesn't really cover the case where the default is expensive to compute (so you want it to be lazy). In Rust, `Map::get` just returns an `Option`, and `Option` supports the relevant services (`unwrap_or` and `unwrap_or_else` in this case), which also means it supports those services for every API which returns an option.
bool fetchFromCache(int userId, UserProfile& profile);
bool fetchFromServer(int userId, UserProfile& profile);
UserProfile profile;
if (!fetchFromCache(userId, profile) && !fetchFromServer(userId, profile)) {
std::cout << "Failed to fetch user profile.\n";
return;
}
or just to prove a point, although even more unreadable than the monads: bool extractAge(const UserProfile& profile, int& age);
UserProfile profile;
int age;
if (
(!fetchFromCache(userId, profile) && !fetchFromServer(userId, profile))
|| !extractAge(profile, age)
) {
std::cout << "Failed to determine user's age.\n";
return;
}
int ageNext = age + 1;
This probably isn't a great example usecase for this, but its helpful when creating composable transform pipelines user_id = 12345
user = fetch_from_cache(user_id) or fetch_from_server(user_id)
age_next = user and (user.age + 1)
if age_next:
print(f"Next year, {age_next} years old")
else
exit("Failed to determine next age")This comment here, I think, has the answer. The inial returns from using the more functional and mathematical view of programs are smaller than that of traditional approach. But the former scales better if you invest more time in it, and eventually can give you much more.
``` ageNext = age.transform([] (const int age) { return age + 1; }); ```
if equally readable to
``` if (age) { ageNext = *age + 1; } ```
Maaaaaybe!
Compare
const auto ageNext = fetchFromCache(userId)
.or_else([&]() { return fetchFromServer(userId); })
.and_then(extractAge)
.transform([](int age) { return age + 1; });
with const auto ageNext = [&]() -> std::optional<int> {
auto user = fetchFromCache(userId);
if (!user) { user = fetchFromServer(userId); };
if (!user) return std::nullopt;
return extractAge(user) + 1;
}();
If you allow GCC/Clang extensions, the first two lines in the lambda can be even turned into auto user = fetchFromCache(userId) ?: fetchFromServer(userId);Example: While programming kotlin, intellij idea warned me whenever I accessed a nullable object without a null check, and gave suggestions to convert types to non-nullable in order to avoid the checks altogether whenever appropriate.
I think there is value in keeping the code clean while keeping it correct at the same time, but that should be done at the compiler or linter level.
auto askForUsername = [&] { … };
auto lookupUserByName = [&](auto name) { … };
auto printUserDetails = [&](auto userId) { … };
auto details =
askForUsername()
.and_then(lookupUserByName)
.and_then(printUserDetails);Enforcing behavior via the typing system prevents bad code from even compiling and running in the first place.
When you stop thinking of Optional<T> as a inconvenient wrapper clumsily wrapping a T and starting thinking of it as a first class datatype with its own members and methods, then it becomes a lot more clear to reason about the logic.
You're right and I've made a fool of myself. My apologies.
Also, the type system in C++, despite all the template stuff, is not actually very good for serious functional programming. For example, when taking a function, there is no generic way to specify its signature in your own signature (and no, taking a std::function is not generic). `require` goes a long way though nowadays.
template <typename T> requires std::invokable_r_v<int,int,int> int foo(T fn){ return fn(10, 20); }
But I think you wanted to show a different example.
And yes the inline version rather than requires is much nicer.
std::optional<int> foo();
And then
std::optional<int> bar(){
int x = co_await foo();
int y = co_await foo();
return x+y;
}No messy chaining or lambda pyramid of doom.
I'm surprised that the monadic ops and C++ co_await are not being aligned in the standard yet
const auto ageNext = fetchFromCache(userId)
.or_else([&]() { return fetchFromServer(userId); })
.and_then(extractAge)
.transform([](int age) { return age + 1; });
what is the meaning of the & in the first lambda?Is there a benefit to the explicit version or is this purely a stylistic matter?
That said, I usually use implicit because if I'm writing a lambda it's in a highly local context where a) it's just a shortcut, b) I know exactly what I'm doing, and c) it's not really meant for usability.
If you want to make the intent really explicit and rigid, it might even be better to use a structured capturing type with an operator(): `MyCapture{.x = x, .y = y}()` or whatever.
It seems like there is no way in C++ to express that i want to traverse the objects themselves and not the container objects. Hopefully that changes soon.