This paper aims to extend monadic operations of std::expected and std::optional by allowing taking extra arguments. These arguments will be forwarded to the given function, passed after the first argument with the contained value. When applicable, the position of the contained value can be controlled using a placeholder mechanism.
Seems like a bad idea to me.
We have two different idioms for passing an invocable in C++:
foo(&f, a, b, c); // (1)
foo([&]() { f(a, b, c); }); // (2)
Idiom (2) is more general-purpose: it allows `f` to be an overload set or a template, or to find `f` via ADL. Idiom (1) is less general and IMHO uglier, because it depends on the reader to realize that here function-invocation is represented by "f, x" instead of "f(x)". I like my function invocations to look like function invocations. (Although I can see how some people might think `[&]` is uglier than `,`, too!)
So, why do we have idiom (1)? Only because some facilities, such as std::thread and std::async, were invented before we had discovered lambdas. (The C++0x cycle was very long, and not everything in C++11 came in at the same time.) So these facilities were designed around idiom (1) because that's the only idiom that existed at the time. Later, we got lambdas and realized that idiom (2) is all you need. Therefore, "post-modern" C++ facilities such as monadic optional/expected were designed around idiom (2), and idiom (1) became a footnote to history.
At first glance, there's a consistency argument here: Plenty of facilities (std::invoke, std::async, std::thread) support both (1) and (2). Monadic optional/expected seem to support only (2). It would be more consistent if they'd, also, support both (1) and (2).
But do monadic optional/expected support (2)? At second glance, I think not. They take a lambda with an argument. So really today we have the possibility of writing
o.and_then([](int x) { return f(a, b, c, x); }); // OK
o.and_then([](int x) { return f(x, a, b, c); }); // also OK
Which of these would be implied by
o.and_then(f, a, b, c); // fantasy syntax
? You arbitrarily pick one, but I don't think there's any logic to your choice.
Meanwhile, you simultaneously propose a whole third way of doing it: `std::placeholders::value`. This would eliminate the arbitrariness above:
o.and_then(f, a, b, c, std::placeholders::value); // fantasy syntax, OK
o.and_then(f, std::placeholders::value, a, b, c); // fantasy syntax, also OK
In fact it would be super-general in allowing you to write:
o.and_then(f, std::placeholders::value, std::placeholders::value, std::placeholders::value); // fantasy syntax, calls f(x,x,x)
o.and_then(f, std::placeholders::value, std::placeholders::value); // fantasy syntax, calls f(x,x)
o.and_then(f, std::placeholders::value); // fantasy syntax, calls f(x)
o.and_then(f); // fantasy syntax, calls f()... uh-oh! this is also real syntax that calls f(x)!
The last line is a problem because `o.and_then(f)` already has a meaning. You can't change that! So, your proposed syntax is actually a generic-programming pitfall.
Even if it weren't for that, the programmer might think it's weird that he can write `f(x, 42)` as `(f, std::placeholders::value, 42)`, but that there's no way to write `f(&x, 42)` or `f(x + 42)`. You could try to add one. At this point you're reinventing std::bind and all its wacky pitfalls. We invented lambdas in order to eliminate this mess of weird DSLs from C++. Don't bring it back!
So you at least have to get rid of `std::placeholders::value`. Having done that, you're back to the problem that your choice of meaning for `o.and_then(f, a, b, c)` (prepend `x`, or append `x`?) is arbitrary. At the very least a real proposal would have to survey prospective users of this feature (I mean not a survey of humans' opinions, but a survey of real codebases that could use a mechanical translation of the existing `.and_then` into this style) and tally up how many of them would benefit from the prepend-`x` convention versus the append-`x` convention. At least then you'd be able to point to those numbers as justification for your choice.
P.S., your Tony Tables aren't very "steel-manned." For example you have this on the left side of one of them:
auto componentResult = extractContext()
.and_then([&path, &metaData](Context context) {
return createComponent(context, path, metaData);
});But that's not equivalent to what's on the right (IIUC). It would be equivalent if it took `Context` by reference, not value.
And it's not the kind of code I would tell anyone to write, either. You're spelling out captures as if the reader needs to beware of some pitfall. But this is the "99% case," where [&] Just Works and you shouldn't waste your reader's brainpower on parsing a capture-list. Let the compiler do the busywork for you!
auto componentResult = extractContext()
.and_then([&](const Context& context) {
return createComponent(context, path, metaData);
});
This eliminates the first two bullet points in the following "Why not just use a lambda function?" Q&A: there is no long capture-list, and there is no long body (because the body is by definition just a single function call, possibly with a `return`). "Many named lambda functions can clutter a function" — lambdas don't have names. "A user has to write more (repeated) code" — example needed. Are you talking about the boilerplate of `[&]() { return`? (If not: should you have?)
–Arthur