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.
In C++ they’re important for safety, because dereference of an empty std::optional is a silent UB. The methods robustly handle the checks for you.
Nested .map in Option or Result in Rust are indeed ugly, but the try operator (?) makes up for it in my opinion
I can't really think of a way in which I'd consider the monadic version better-or-equal than the "traditional approach". Except that maaaaybe
``` ageNext = age.transform([] (const int age) { return age + 1; }); ```
if equally readable to
``` if (age) { ageNext = *age + 1; } ```
Maaaaaybe!
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.
Why don't they add a new interface which will essentially be Monad? Is the polymorphism story in C++ not good enough for practical use of it?
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'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.
But code using these monadic operations can also be quite clean… toy example:
auto askForUsername = [&] { … };
auto lookupUserByName = [&](auto name) { … };
auto printUserDetails = [&](auto userId) { … };
auto details =
askForUsername()
.and_then(lookupUserByName)
.and_then(printUserDetails);
The C++ standard standardizes existing practice; it's not a place for innovation.
Monads without proper syntax (do-notation) are kinda ugly though
If I didn't learn monads with haskell before, I would think that monads is some useless boilerplate abstraction