Boost logo

Boost :

Subject: Re: [boost] [review] **NEXT WEEK** Review of Outcome (starts Fri-19-May)
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2017-05-15 11:08:01


>> The major refinement of outcome<T>, result<T> and option<T> over
>> expected<T, E> is that you **never** get undefined behaviour when
>> using their observers like you do with expected<T, E>. So when you
>> call .error() for example, you always get well defined semantics and
>> behaviours.
>
> I fully agree with this design decision of yours, by the way. In fact I
> consider that a defect in expected<>.

It's the fault of std::optional<T>. In my opinion it never should have
had silent reinterpret_cast built into its API like that. The cost of
the state check is unmeasurable on any recent CPU, it's not worth not
having.

I've been a strong advocate that the same mistake not be repeated with
expected<T, E> with Vicente, but I have failed to shift his opinion. He
wants optional<T> equivalence, and I can see where he is coming from
(though I think optional<T> should get fixed instead, even if it breaks
existing code).

Another defect in Expected in my opinion is having value return
semantics for .value_or(). You'll note Outcome's Expected has reference
return semantics for .value_or() which is a deviation. Again, they only
chose value semantics because of optional<T>, and I have tried to ensure
they don't repeat the same mistake. I may have a chance of winning that
one however, they will discuss it in Toronto.

Finally, std::expected provides comparison operators which makes no
sense to me at all. If you are putting Expected's into a map, you need
your head examined in my opinion. Outcome's Expected does not provide
comparisons except for equality.

>> you get a C++ exception thrown of type monad_error(no_state).
>
> As a side note, it would be nice from my point of view if you eradicate
> these last remaining references to 'monad' in the public interface and
> make that outcome_error (resp. outcome_errc, outcome_category.)

A lot of people grumble at having "monad" in the naming. I don't
personally see the problem, it's just a name, and "basic_monad" is
exactly what is it: a building block for a monad.

I'll tell you what though, if two more people strongly feel that
"basic_monad" and "monad_error" need to be renamed, I will do it. Please
people do suggest a better alternative name though.

>> outcome<Foo> found; // default constructs to empty
>> for(auto &i : container)
>> {
>> auto v = something(i); // returns a result<Foo>
>> if(v) // if not errored
>> {
>> found = std::move(v); // auto upconverts
>> break;
>> }
>> }
>> if(!found)
>> {
>> die();
>> }
>
> OK, let's go with that. Why not construct 'found' initially to contain
> some error, instead of being empty? You can even define a special errc
> constant to denote an empty outcome.

You can ask it to be some error too:

outcome<Foo> found(make_errored_outcome<Foo>(std::errc::whatever));

If you didn't like the lack of explicit initialisation, you can also do:

outcome<Foo> found(empty); // constexpr empty_t empty; let's you tag

But the point being made (badly) in the example above was that you can
use the empty state to indicate lack of find, and any errored state for
errors during find etc. So, let me rewrite the above now it's morning
and I am not babbling incoherently:

outcome<Foo> found(empty);
for(auto &i : container)
{
  auto v = something(i); // returns a result<Foo>
  if(!v.empty()) // if not empty
  {
    found = std::move(v); // auto upconverts preserving error or Foo
    break;
  }
}
if(found.has_error())
{
  die(found.error());
}
if(found)
{
  Do something with found.value() ...
}

The above is much better. Please disregard what I wrote last night.

> What I'm driving at is that these result types are conceptually (T|E)
> and the empty state could just be a special case of E.

I did consider that design, but I rejected it. outcome<T>, result<T> and
option<T> all require E to be some form of error_code_extended, so the
user can override E, but not significantly change what contract it
promises. You might think then there is an ideal scope for !E to mean
empty, but that it turns out is a bad idea. A null error code has the
convention of meaning "no error occurred". It does not mean "empty".

You could define a special error code category to mean "empty", but now
you break all other code using error_code because there is no
error_condition which represents "empty".

Finally, if you did have some special E value to mean empty, you would
have to write special checks for it in Outcome in order to give it the
stronger abort type semantics it has - if you don't have it being given
alternative semantics, then there is no point in having an empty state.
If you are writing special checks, then you might as well just have a
formal empty state in the first place.

This argument can be generalised into the argument in favour of ternary
logics over binary logics. Sure, 90% of the time binary logics are
sufficient, but binary is a subset of ternary. And *you don't have to
use* the "other" state in a ternary logic just because it's there. Just
don't use it, so long as its implementation signals its unintentional
usage with a very loud bang, it's a safe design.

(There is an argument that Outcome's "very loud bang" isn't loud enough.
I'd be pleased to hear feedback on that)

> Or, in more general terms, I feel that there's still much extra weight
> that can be stripped off (not in terms of sizeof, but in terms of the
> interface.)

As you have surely noticed, I have (intentionally) provided many
undisambiguated ways of using Outcome, pushing the problem of which
specific combination to use onto the end user.

That looks sloppy I am sure. None of my other libraries do that either,
they provide one or two "clean" ways of doing any given thing, Outcome
is the only library I've done where I give the end user enormous scope
to choose their particular style and idioms and conventions and I give
no guidance as to which to use (I use different conventions in AFIO v2
and KernelTest myself personally).

But it was not done without reason. During my ACCU talk, about half the
audience really want implicit conversion for T and E in Expected so you
can skip typing make_xxx() all the time. The other half want always
explicit instantiation so that it is impossible to create an Expected
*without* using make_xxx() (BTW, Toronto will be discussing enforcing
this for std::expected i.e. to remove all implicit conversion)

The same goes for C++ monadic programming frameworks. There are many
diametrically opposed opinions, and no clear evidence which is right and
which is wrong. Outcome's monadic API (disabled in this presentation)
deliberately sits on the fence and provides just enough hooks to be used
by any external monadic programming framework. It doesn't implement a
full monadic programming DSEL. I intentionally don't choose sides nor
favourites. That upsets almost everyone, but my personal opinion here is
that there is no one right answer, sorry.

Similarly I intentionally sit on the fence with all the rest of Outcome:
it is up to the end user to choose the idiomatic style they prefer.
That's why the public API looks "heavy", but really it's just many
syntax ways of doing exactly the same thing. The core is extremely
simple to keep compiler optimisers happy and generating least possible
runtime overhead or code bloat which it very successfully achieves.

Ultimately what I would ask is instead of "is this API too fussy?" is
rather "is this API unsafe?" or "is this API self contradictory?". I am
very interested in feedback on the latter two.

Niall

-- 
ned Productions Limited Consulting
http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk