C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Extend monadic operations of std::expected and std::optional by allowing taking extra arguments

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Sun, 5 Jul 2026 21:46:06 -0400
On Sun, Jul 5, 2026 at 5:46 PM Vitaly Fanaskov via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> 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.
> https://github.com/vt4a2h/ipa
>

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

Received on 2026-07-06 01:46:21