I quite enjoy the idea of monadic types, and have written (beaten?) some into languages that dont have native support, but I cant help but wonder if this (and when I see rusts too) approach of `.if_then`/`ok_or` `.or_else` etc actually improve readability?
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.
> I cant help but wonder if this (and when I see rusts too) approach of `.if_then`/`ok_or` `.or_else` etc actually improve readability?
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.
Tangentially related, but if you don't have access to c++23 or you don't like monadic properties, passing by reference and returning bool success can emulate or_else, transform, and_then with || &&
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")
After reading this kind of code for a week it becomes as easy as anything else. As long as there’s no pyramids of doom (possible with plain if else, promise .then and async/await and this optional and_then) code will be easy to read.
That's true for many things we as humans can become accustomed to. But what exactly is the (hopefully quantifiable) improvement here? Otherwise, it's just a style option of preference.
If you use the optional type, then nothing. It’s just style. If you don’t use it and your compiler can tell you’re dereferencing a possible null value, then again you gain nothing.
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.
For single operations, maybe (and still it's a style, you get used to it), but the value comes from chaining. For me, the user/ageNext example is much more readable with monads. That might also be because I'm used to monads with Rust.
I've seen early proposals of this and never been a big fan of the syntax, I'm disappointed it made it in to C++23. Monads can be useful if they enable composition across different concepts, but this is syntactic sugar that IMO can be expressed in a nicer way with an immediately evaluated lambda and early returns.
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);
I've been using the equivalent function in Rust for some time with great success. I think it's a good addition to C++ as well, and I'm looking forward being able to use C++23 for that kind of things.
That looks like something I'd have concocted 20 years ago. Oh yeah let's chain expressions with functors in order to wrap the behaviour of a simple if statement. Campus party at 8.
Hardly anything new to anyone using ML or Lisp derived languages for the last half century, it is still quite new concept for most folks, specially in languages like C++.
The idea is to make the accesses safer, not necessarily more readable. The monadic operations make it impossible to access the value in a std::optional without first testing that it actually contains a value.
I understand the value of enforcing behaviours with types, but experience has taught me that this is a tooling issue, not something you have to wrap into a spaghetti mess every time you want to code an if.
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.
A linter or IDE can only "suggest" that you don't access null objects without a null check. You as the developer can choose to ignore the linter or IDE and write bad code anyway.
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 really did not. Common practice and innovation are the same thing. Look at lambdas. Then polymorphic lambdas. Then async. Then modules. Innovation is obviously getting in. Parent commenter was being a pedant. Doesn't matter if he's stroustrup himself.
I don’t like the so-called fluent interface. And control structures duplicated by methods are ugly as well. RIP command-query separation and clear OOP semantics.
It's not that you couldn't do that, it's that polymorphism (in the interface/virtual functions sense) is only one of several paradigms for abstraction/code reuse in C++. And it is on the higher side of runtime cost among them.
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.
I'm on mobile so maybe I mixed the concept and the trait but there should be an invocable_r version that constrains the return type in addition to invocable which just constrains the arguments.
And yes the inline version rather than requires is much nicer.
The `&` captures the surrounding variables by reference. This means that the lambda will have access to variables defined in the scope where the lambda is declared, without making a copy of them. In this case, `userId` is captured by reference, so any changes to `userId` outside the lambda would be reflected inside the lambda as well.
Implicit capture may capture more than you intended to. Explicit will cause the compiler to alert you to other names you might not have realized you included. It also makes it harder for people modifying your code to mess it up.
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.
Implicit captures may not be obvious and may be accidental, leading to lifetime issues. Without a borrow checker like rust, it's safer to name them explicitly most of the time.
I wish we had the option to specify explicit captures in rust closures too. When you need it you need it, and it is good for clarity. The current "add `move` to switch from by-reference to by-value" is too coarse grained
Some time ago I was wondering what would be the best way to traverse a list of objects contained in optionals or unique_ptrs.
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.
If I didn't learn monads with haskell before, I would think that monads is some useless boilerplate abstraction