The major refinement of outcome<T>, result<T> and option<T> over expected
is that you **never** get undefined behaviour when using their observers like you do with expected . 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
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/