On 05/25/2017 07:28 AM, Vicente J. Botet Escriba wrote:
Le 24/05/2017 à 21:44, Thomas Heller via Boost a écrit :
Hi Niall (and also probably Vicente) I believe that this should be discussed probably in the std-proposal ML. Anyway as we are here
Sure, whatever you think is best :)
I was following the discussion about expected/outcome/result very closely and I can absolutely see the usefulness of such a library (I explicitly discarded option here).
I am not sure if this post should count as a review, mainly because I disagree with the fundamental design decisions (that includes expected as defined in D0323R2) and therefor would cast my vote as in "not ready yet", I hope I can convey my points below. Wow, first notice I have of this disagreement. It is better later than never.
Still not sure if everything I said really makes sense, I just felt this line of thought might be interesting to the general discussion.
First of all, I don't agree with the strong (conceptual) relationship between optional (be it boost:: or std::experimental) in such a way that expected is a generalization of it. From what I understand, the purpose of expected/outcome/ result is to be used as a mechanism to return the result of a function. As such it should also expose the semantics of it. Fortunately, we already have a (asynchronous) return object standardized (std::future). And this is my basic disagreement here: Why not model expected in the lines of future? With the main points being: - expected being movable only - expected<T,E>::value() should always return by value and invalidate this. - (I would really prefer the color .get though ;)) So the question to Vicente/Niall is: what is the motivation to make it "optional-ish"? Do we have use cases which make this design undesirable?
expected is a generalization of optional as it is synchronous and could return more information about why the the value is not there.
Right, I can see this argument now, let me try to rephrase it a little bit (correct me if I got something wrong please): We want to be able to have a mechanism to return (or store) a value which might not be there, and we need to know why it is not there. The class that's currently available which almost does what we want is optional, in fact, it is already used in such situations, so what we miss is the possible error. So here we are, and then we naturally end up with something like variant<T, E>. Makes perfect sense. My line of thought was mostly influenced by the property of being solely used as a return object. And well, we already have the asynchronous return objects, so why not go with something a synchronous return object which represents a similar interface/semantics. With that being said, I am still not sure if the result of both ways to look at it should converged into the same result.
expected could be seen as the ready storage of a future. future::get block until the future is ready and then returns by reference :)
Except not quite ;) excepted indeed sounds like the perfect fit for the value store in the share state. The only problem here, is that it really requires some kind of empty (or uninitialized) since the value is only computed some time in the future (this is motivation #1 for the proposed default constructed semantics), having either a default constructed T or E doesn't make sense in that scenario. So it is more like a variant<monostate, T, E>. The weak point in my proposed interface would indeed be the destructive .value() functions, since a shared state needs to be able to obtain it's value multiple times (in the case of shared_future).
I want to ask you, what would be the liabilities of an expected that is copyable?
That's a good question. When initially thinking about it, I was very deep into the semantics of asynchronous return objects, which use a common shared state which presents an entirely different problem. So I was always thinking in terms of: Does a copy share the result, like shared_future, or does it have to be unique? I guess this doesn't apply to expected and keeping the value semantics of the underlying types makes most sense. I guess there is nothing wrong with expected being copyable if T and E are copyable.
We don't have a problem returning be reference, why would we like to return by value?
Mainly to represent the fact that we have a return value from a function. There is always one return, if you want to alias it, bind it to a named variable.
Why do you prefer get? what do you get with get? How will you name the function that give you access to the value of a PossiblyValued type?
It's really just a different color. The preference to get is coming from my mental model of mine with expected being more like a asynchronous return object (future calls it get as well). There are other types (with similar purpose) using the name get for their accessor: shared_ptr, unique_ptr, tuple and variant.
By having these constraints, expected of course needs to have an uninitialized state. As such we'd have the three observers: valid(): true when has_value() || has_error(), false otherwise (for example default constructed, invalidated), has_value() and has_error().
Sorry but, not. We don't need such state. This is something future needs, but not expected.
This state is the logical conclusion coming from the destructive value function. As a result, one could just as well reuse it for default construction. If there is consensus that value returns by reference, I would go for result not being default constructible and have never-empty guarantees.
I we decided to default construct to an uninitialized state, I wouldn't support to show to the user this state via any observable function, but via UB (as for chrono::duration).
That's fine for me as well...
Second, I think Niall raised a valid about outcome being a framework for interoperability (completely orthogonal to the first point). However, I totally miss this from the proposed library, most pressing are non intrusive mechanisms. For that purpose I postulate, that a mechanism to transform between different unexpected results, that is: various error codes etc. However, for that to work, one would of course need a properly defined concept, for example, as Vicente suggested "EitherValue", and a mechanism to coerce one error type into another, maybe through ADL, or traits specialization or whatever.
There will be such a proposal as a generalization of Nullable based on what I named PossiblyValued.. https://github.com/viboes/std-make/blob/master/doc/proposal/nullable/D0196R3...
That's a good starting point!
With that in place, one could simply define the different EitherValue types, there is no need that everything needs to be in the form of "basic_XXX". For the library under review, this would be perfectly sufficient: template <typename T, typename E> class expected; template <typename T> using result = expected<T, extended_error_info>; template <typename T> using outcome = expected<T, variant<extended_error_info, exception_ptr>>; We will need to have a specialization of expected<T, varaint<E...>> as we have an index for variant. The revision 2 of the expected proposal talks of a expected<T, E1, ..., En>.
That is, given that we have either a value or unexpected, we can convert expected<T, E1> to expected<U, E2> if T is convertible to U and E1 "coercable" (with whichever mechanism) to E2. I have added this conversion constructor recently to the expected proposal as the result of my understanding of the need of Outcome and I hope we will discuss it in Toronto.
Cool!
If we then have a generic mechanism to get from a (possibly user defined "E") to an exception, I completely miss the point of the outcome template.
And why not to throw E?
throw E; is certainly a nice default, but it really should be customisable. For example, for std::exception_ptr, you probably want to throw the underlying exception? What if you have exceptions disabled, do you want to give users the chance to implement whatever they want?
All optimizations can then easily be put as implementation details and the generic expected<T, E> will probably suffice for most use cases, for everything else, we can implement special types which conform to our concepts and implement the error conversion mechanisms. This will most likely also work with different APIs/ABIs.
The main problem is that we don't have here the generic interface and it is for this reason we are discussing on the details of a concrete class. The original expected proposal has fmap, bind, catch_error functions. We have removed them form expected since the last revision, but we need now to have a generic interface for those functions.
Those should be the next step once we have the underlying concepts ready. I was made aware of a very interesting experiment recently ... mainly to use the mechanisms defined in the coroutine TS for optional. Ignoring that co_await sounds awkward when dealing with PossiblyValued objects, it almost gives you everything you need to handle those objects with the nice syntax people usually refer to as "monadic".
For me expected should have the minimum, everything else should be associated to a specific concept, as Nullable, PossiblyValued, MonadError, SumType.
https://github.com/viboes/std-make/blob/master/doc/proposal/monads/Monads.md
https://github.com/viboes/std-make/tree/master/include/experimental/fundamen...
https://github.com/viboes/std-make/tree/master/include/experimental/fundamen...
https://github.com/viboes/std-make/tree/master/include/experimental/fundamen...
I guess we are not fundamentally disagreeing. I would really like to see "monad" to vanish again from our vocabulary though, while it is certainly a nice underlying theory, C++ just misses too much to ever really define Monads properly without making fun itself (Type Categories?, Concepts are not really the same...).
Vicente
-- Thomas Heller Friedrich-Alexander-Universität Erlangen-Nürnberg Department Informatik - Lehrstuhl Rechnerarchitektur Martensstr. 3 91058 Erlangen Tel.: 09131/85-27018 Fax: 09131/85-27912 Email: thomas.heller@fau.de