fenbf / cppstories-discussions

4 stars 1 forks source link

2024/pipe-operator/ #140

Open utterances-bot opened 1 month ago

utterances-bot commented 1 month ago

Function Composition and the Pipe Operator in C++23 – With std::expected - C++ Stories

In this blog post, we’ll show how to implement a custom pipe operator and apply it to a data processing example. Thanks to C++23 and std::expectedwe can write a rather efficient framework that easily handles unexpected outcomes. This is a collaborative guest post by prof. Bogusław Cyganek: Prof. Cyganek is a researcher and lecturer at the Department of Electronics, AGH University of Science and Technology in Cracow, Poland.

https://www.cppstories.com/2024/pipe-operator/

sehe commented 1 month ago

If you're going to construct expensive Mersenne Twisters on the fly, please use a proper distribution to sample it. Right now you've basically recreated std::rand() % 2 with a possibly longer period but way way higher cost and still the same conceptual problems of any rand() % N

sehe commented 1 month ago

Still a bit confused. This seems to be syntactic sugar around std::expected's new monadic operations. Indeed you mention it at the very last minute. As such RACO seems a misnomer?

It appears to have nothing to with ranges (it should in fact be orthogonal) and to me it makes sense to expect this to use a different operator (like >> or >>= as many similar libraries have used over the years).

BogCyg commented 1 month ago

Thanks for reading and your valuable comments! There is a lot of truth in what you write, but please remember that this is only an example code, which necessarily has to be short. In more refined versions, of course I do not recommend creating and initializing std::mt19937 every time the function is called. However, using the engine itself, which is a high quality pseudo-random generator (https://en.cppreference.com/w/cpp/numeric/random/mersenne_twister_engine) without the distribution object, is ok and is not the same as using rand() %N as you write. We should no longer consider using rand at all – see e.g. a very interesting talk by with Stephan Lavavej (https://learn.microsoft.com/en-us/shows/goingnative-2013/rand-considered-harmful).

Regarding RACO – this stands for Range Adaptor Closure Objects which has been introduced in the context of the std::ranges library (https://en.cppreference.com/w/cpp/named_req/RangeAdaptorClosureObject). RACO are simply function objects that are callable via the pipe operator. Therefore it is good to know this term when talking about custom pipe operators, since we also use callables in our pipe overloading.

For other overloaded operators, such as >>=, I’d recommend C++ PIPES by Jonathan Boccara (https://github.com/joboccara/pipes). However, please remember that these operators have different precedence and associativity.

sehe commented 1 month ago

@BogCyg thanks for the high quality references. Regarding the use of the random generator, my point was about skewed distribution when using modulo operator: https://stackoverflow.com/a/10984975/85371

Regarding the name, I guess you're saying "I didn't invent the misnomer" :) Fair. I prefer when people avoid misnomers. It tends to "hammer/everything is a nail" thinking ("optional is a 0/1-element range!"). Functional programming language get this right. It's easy to fix before standardizing. (_On quick scan https://en.cppreference.com/w/cpp/named_req/RangeAdaptorClosureObject seems to explicitly state that the components in the chain must be ranges, by the way._)

I'll have a look at the excellent links.

StefanoBell commented 1 month ago

Maybe it's a little bit off topic 'cause I'm going to talk abou std::expected rather than of the pipe operator... anyway, a problem I found with std::expected (and it's the same for std::optional) is when it is used as the return value of a function that could fail (hence an error state) but that would return a boolean value for the happy path. For example, say we have a function that checks whether a surface is planar, defined like this:

std::expected<bool, ErrorCode> isPlanar(Surface surf);

now, a call like the following is misleading:

if (!isPlanar(surf))
{
   // do something
}

because it is actually checking that the function is not raising an error but it looks like it is checking for the planarity of the surface. Now, one could say that for failures in these cases we should use exceptions, but if an entire API is defined in terms of std::expected doing an exception (pun not intended :-)) for when it is the case to return a bool is unfortunate to say the least. Probably, std::expected shouldn't have defined an explicit operator bool (same for std::optional...).

BogCyg commented 1 month ago

That's an interesting case to have 'a value' also being bool. However, (i) you can use the has_value() member function of std::expected, also (ii) operator bool is defined explicit, see https://en.cppreference.com/w/cpp/utility/expected/operator_bool constexpr explicit operator bool() const noexcept; constexpr bool has_value() const noexcept;

StefanoBell commented 1 month ago

Oh, that's right. But admittedly the habit to write !func() rather than func.has_value() is something hard to kill. And I've already spot a few bugs (in unit tests) exactly because of this "issue". Now, for the rest, I think std::expected is great. I'm complaining about the specific case of std::expected<bool, E>.