5 ms·
How to Use Monadic Operations for `std:optional` in C++23
- jgilias 3y agoWhat a time to be alive!
- ogoffart 3y agoI'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.
- deleted 3y ago[deleted]
- nurettin 3y agoThat 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.
- throw156754228 3y agoMore categorical solutions looking for a problem.
- pjmlp 3y agoHardly 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++.
- mmoll 3y agoThe 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.
- nurettin 3y agoI 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.
- mmoll 3y agoBut 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);
- deleted 3y ago[deleted]
- robot_no_421 3y agoA 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.
- mgaunard 3y agoThe C++ standard standardizes existing practice; it's not a place for innovation.
- mmoll 3y agoHuh? Who says so?
- jeroenhd 3y agoThe people writing the C++ standard seem to disagree.
- mgaunard 3y agoI have been part of the C++ standards committee for 13 years.
- jeroenhd 3y agoYour comment has led me down a rabbit hole regarding the C++ standards commission and corrected various misconceptions I've held for a while. You're right and I've made a fool of myself. My apologies.
- nurettin 3y agoYou 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.
- deleted 3y ago[deleted]
- deleted 3y ago[deleted]
- bongobingo1 3y agoI 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.
- sakex 3y agoNested .map in Option or Result in Rust are indeed ugly, but the try operator (?) makes up for it in my opinion
- deleted 3y ago[deleted]
- masklinn 3y ago> 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.
- pornel 3y agoIn 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.
- deleted 3y ago[deleted]
- klntsky 3y agoWhy 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?
- MauranKilom 3y agoIt'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.
- injidup 3y agoYes requires is pretty powerful with std::invokable but if one of the parameters of your passed function is also generic then it gets more hairy. template <typename T> requires std::invokable_r_v<int,int,int> int foo(T fn){ return fn(10, 20); }
- gpderetta 3y agoint foo(invocable<int,int> auto fn){ return fn(10,20); } But I think you wanted to show a different example.
- levzettelin 3y agoI 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!
- nitnelave 3y agoFor 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.
- nurumaik 3y agoMonads 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
- m1r 3y agoGood stuff but ugly.
- efitz 3y agoMaybe I’m old and set in my ways but I don’t see where the monadic example provided is easier to read or interpret than the traditional example.
- gnull 3y agohttps://news.ycombinator.com/item?id=37714144 https://news.ycombinator.com/item?id=37714144 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.
- vjerancrnjak 3y agoAfter 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.
- NBJack 3y agoThat'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.
- vjerancrnjak 3y agoIf 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.
- nick__m 3y agoIn that code from the article: 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?
- grumbel 3y agoThe & captures the `userId` by reference and makes it available in the lambda body.
- yrro 3y agoIt causes userId to be captured by reference. Compare with [=] which would capture by value.
- nick__m 3y agoThank you grumbel and yrro, I get it. I'm used to write things like [&userId]() and was ignorant of "implicit" capture. Is there a benefit to the explicit version or is this purely a stylistic matter?
- binary132 3y agoImplicit 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.
- surajrmal 3y agoImplicit 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.
- ot 3y agoI'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);
- tipsytoad 3y agoTangentially 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
- d0mine 3y agoIn Python, it would be: 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")
- LudwigNagasena 3y agoI 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.
- deutschepost 3y agoSome 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.
- injidup 3y agoI'm surprised nobody mentioned co_await yet. This should be possible. 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