C++ Logo

std-proposals

Advanced search

Fwd: P2192R0 -- Transparent return type

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Wed, 29 Jul 2020 12:54:44 -0400
---------- Forwarded message ---------
From: Jason McKesson <jmckesson_at_[hidden]>
Date: Wed, Jul 29, 2020 at 11:47 AM
Subject: Re: [std-proposals] P2192R0 -- Transparent return type
To: <dbj_at_[hidden]>


On Wed, Jul 29, 2020 at 7:22 AM Dusan Jovanovic (DBJ) <dbj_at_[hidden]> wrote:
>
> Hi Jason,
>
> many thanks for the comprehensive critique on style especially. It will shape the R1 certainly.
>
> now ... the core idea is "metastate" .. it has nothing to do with C++20 "concept"'s. function return metastate is not a state .. state would be "error or no error". one can capture it, but metastate is not a type. perhaps I should use the synonym: "theory" or simply "core idea".
>
> Technically std::valstat" is just a meta state carrier (an vehicle). Implemented in C++ using C++ std lib.
> It is equally valid (aka "core idea compliant") to implement it in C ... as the article shows. Or any other imperative language. One can imagine the whole new CRT implementation using the valstat core idea.

Ideas in programming are useful to the extent that there is support
for those ideas. The idea of an object which may or may not be present
is interesting, but its actual *utility* is defined by how you
implement it. One could implement such a type as just a struct
containing a public boolean and a union of `char` with `T`. But that
would be a terrible type.

`std::optional` comes with a bunch of features. It allows in-place
construction, assignability, and so forth. Something as simple as
conversion from the `std::nullopt` value enhances its readability and
utility for the user tremendously.

To the extent that I find your metastate idea interesting, I find your
*implementation* of that idea to be so feature-poor as to be
effectively worthless. It is a type which provides no actual features,
for which your metastate idiom is defined implicitly-at-best, and for
which

> What do you think on other benefits of using metastate (aka valstat) like "no exceptions"? I might say "no exceptions for free".

I kinda skipped over this in my first pass, as it seemed unimportant.
But you've doubled-down on it, so I feel the need to address it.

In at least two places, you speak about your type as if it enables
something that could not have been done otherwise. Here, you claim
that it allows "no exceptions". In your proposal, you also mention
that it lets you write "pure functions".

As if C++ programmers haven't been doing both of these for decades.

Indeed, the whole point of this type is to try to unify the various
idioms C++ programmers have developed for writing such functions. That
means we already have ways to achieve it, but they are all different
ways with no real unity between them.

So the benefit of adopting `valstat` (hypothetically) is not that you
can do something you *couldn't* before; it's to have a *standard* way
to do that thing. And it's important to say it that way.

> What do you think of the attempt to conform to the P2000R1 recommendations?

I'm not really sure why you even brought that up. To the extent that
`valstat` fulfills something on that list, `expected` and `outcome`
*also* fulfill that same niche.

> lastly comparing valstat metastate idea to std::expected or outcome is "comparing apples and pears" .. I should clearly explain it in the doc.

But they're not *that* different. You are clearly playing in the same
playground as `expected` and `outcome`. Oh yes, `valstat` has specific
support for "Value+Status" returns while the others don't. But that's
really the only difference between them from a user perspective.

All of these ideas are fundamentally doing the same thing: using a
single return value to represent both the *actual* return value and an
error code. That is central to your proposal; it is also central to
`outcome` and `expected. So however much you want to talk about
"metastate", the central idea of your proposal is not that different
from those other types.

So comparisons will inevitably be made. The types your proposal
defines are going to have to justify themselves, not just generally,
but relative to those alternative ways of doing more-or-less the same
thing. And simply saying that your type fully implements your
metastate notion and theirs don't allow certain aspects of metastate
is not a particularly good argument in favor of yours.

You have to at least explain why all four states are important enough
(and universal enough) that the best Value-and/or-Error type is one
that implements all of them.

------------------------

After thinking about your proposal for a while, I've come to the
conclusion that your primary focus is on interoperability: having a
single idiom that a bunch of code can use for returning values&errors
through the return channel.

There are basically two ways to go about creating such interop. The
way your proposal picks is to create a single type that provides all
of the features. So if everyone just uses that type, then everyone has
all of the features, there's only one way to return value+errors
without using exceptions, and there's complete interoperability
between code.

Your proposal will never achieve that. Part of the reason is what I've
mentioned before: `valstat` is a terrible type that offers no features
and is only useful to the extent that *other people* also use it. But
since it offers no features, nobody will start using it, and thus no
interop happens.

But the other part of the reason is that, even if `valstat` were the
most wonderful version of your metastate idiom, with a good interface
and excellent features... it's still not the right type for most
users. It's over-designed and overly generic.

Your metastate idiom defines 4 conceptual states: Value, Error,
Status, and Empty. But not every function that needs to produce such
states will produce *all* such states. `to_chars` will only ever
produce Value or Error, never Status nor Empty. `from_chars` can
return Status as well, but never Empty. So... how is a user expected
to know that `to_chars` will never use the Status state? Do they have
to read the documentation? That's not a good mechanism.

I mentioned that there were two ways to create interop. Your proposal
creates interop through a shared type that everyone has to use. I
would suggest instead that you create interop through a shared
*interface*.

Allow people to use whatever types they want. But these types can
expose interfaces that are shared between them. And most important of
all, the types can implement different *subsets* of the full metastate
idiom.

In this idea, "metastate" now becomes an actual concept (though I
think "metastate" is a *really* bad name for it). A type fulfills
"metastate" if it explicitly chooses to do so (types cannot
accidentally be metastates), and if it exposes *at least two* of the
states. All metastate types must expose Value, but they must also
expose at least one of Error, State, or Empty.

Then, you have customization point objects (much like in the Ranges
part of the C++20 standard) which act as interfaces to metastate
objects. The key questions you can ask of a metastate object are:

* has_value
* get_value
* has_status
* get_status
* has_error
* get_error
* is_empty

All the user has to do to enable metastate functionality is define a
few accessible functions of their type and write a single
specialization in `std::` to permit the type to be a metastate.

`std::optional` can expose metastate as it permits Value and Empty.
`expected` can expose Metastate as it permits Value and Error, as can
`outcome`. Your `valstat` type exposes the full range of states.
`to_chars_result` provides Value and Error, while `from_chars_result`
provides Value, Error, and Status.

And they all work through the exact same *interface*. Code consuming a
metastate, particularly in a generic context, might look like this:

```
auto maybe_value = expression;
if(metastate::has_value(maybe_value))
{
  if(metastate::has_status(maybe_value))
  {
    //Do something with status.
  }
  //Do something with value.
}
else if(metastate::has_error(maybe_value))
{
  //Do something with error.
}

```

This can also facilitate visitation APIs. The metastate machinery can
build visitors for them without users having to write special code to
enable them. If `inspect` proposals get into the standard, then the
metastate concept machinery can automatically build the necessary
functionality to do `inspect` stuff with them.

We can also build useful usability features. We can have a
`metastate::error<E>` type which is implicitly convertible to any type
fulfilling the metastate concept that exposes the Error state (with
the same type as `E`). That way, we can `return
metastate::error(expression);` that works universally with any
appropriate metastate type. This allows us to gain the benefit of
`std::unexpected`.

By building a bunch of machinery around the metastate concept, we
encourage interop. Not by making a single type that subsumes all of
their own types, but by making a single interface that subsumes all of
their interfaces. If you make that interface good enough with useful
features, everyone will just play along. They get to keep their own
types tailored to their needs, but we have the interop functionality
we need.

Received on 2020-07-29 11:58:15