Boost logo

Boost :

Subject: Re: [boost] [outcome] On the design and documentation
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-26 13:30:11


Le 26/05/2017 à 12:22, Thomas Heller a écrit :
> On 05/26/2017 09:03 AM, Vicente J. Botet Escriba wrote:
>> Le 26/05/2017 à 08:22, Thomas Heller a écrit :
>>> On 05/25/2017 07:28 AM, Vicente J. Botet Escriba wrote:
>>>> Le 24/05/2017 à 21:44, Thomas Heller via Boost a écrit :
>>>
>>>>>
>>>>> 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>.
>> From a high level I will see it as optional<expected<T,exception_pr>>.
>> We optional because we have in addition the not-yet-ready state. Once
>> the future becomes ready we have expected<T,exception_ptr>.
>> I understand that the implementation optional<expected<T,exception_pr>>
>> could be variant<monostate, T, exception_ptr>. The implementation of
>> expected<T,exception_pr> could store as well variant<T,exception_pr>,
>> but this makes difficult to extract the ready value as
>> variant<T,exception_pr> is not convertible from variant<monostate,
>> T,exception_pr>. So there are two possibilities for future::then
>> * status-quo pass the future as parameter of the continuation
>> * make it possible to cast variant<T,exception_pr> from
>> variant<monostate, T,exception_pr> and pass expected to the continuation
>>
>> I proposed other future functions (next and catch_exception) long time
>> ago to manage with the success and failure branches, so that we don't
>> add any constraint in the way the contents is stored.
>>
>> Resuming, you are right expected<T,E> will not help as a value type
>> representing the ready state without major changes.
>
> Ugh, I don't want to go this path ;)
Which one?
> Mind you, a ready future (which you have present in a continuation)
> already fulfills (or should) all the PossiblyValued properties. If it
> gets ammended with a has_value/has_error/get_exception function, why
> introduce another type? This safes you all the hassle you have with
> the different possible conversion paths.
Humm, the problem are the pre-conditions.
Defining has_value/has_error/get_exception on a Future will need to
check if it is ready :( I know that depending on the implementation this
could be an additional check or not, but at the high level we add an
additional condition.
If you pass a type expected<T,E> that already satisfy the ready
condition, there are no need for such pre-condition. The type system
help you.
If you pass a future to the continuation you can not reuse the
continuation function as it is valid only in this context where the
future is ready.

Is like if you pass a pointer to a function when the function is waiting
for the pointee and it has a sense only when there is a pointee and not
when the pointer is null.
As if the for_each algorithm requested a function that takes the
iterator instead of the value_type.

The argument of passing a future instead has been that the compiler will
optimize all this or that this are micro optimization as concurrency
time is at another level. I believe however that the type system should
be used for this purposes.

Maybe I'm wrong and I've missed something important the current
future::then can provide and that other alternatives cannot.

Please, note that I don't have a complete solution without as I said
major/controversial changes.

>
>>>
>>> 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.
>> Glad to see we agree.
>>>
>>>> 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.
>> I see this as tuples and structured binding. We return a tuple/expected
>> and we are able to get the contents by reference. No need to copy while
>> you want to access to the elements.
>
> Indeed, no need to copy. Assuming Movable, this would work. I concur
> that nowadays you don't necessarily need Movable to return from
> functions...
> In the end, I am fine with either choice.
Great.
>
>>>
>>>> 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.
>> + tuple
>>
>> These get functions are not uniform. Some can throw some not. Some
>> return a pointer some return a reference. Some are narrow some are wide.
>> Should we use a different name for each flavor? I believe we should,
>> but it is too late.
>>
>> If you had to name a function that takes a unique_ptr and returns the
>> pointee value or throw if nullptr, how would you name it.
>> I'll name it value() even if value is not a name.
>>
>> Note that future::get does two things: wait until ready and then
>> returns a reference to the success value or throw.
>> In some way future::get could be named future::wait_value. Too late
>> also.
>
> I really don't want to get into naming discussions, I'll take whatever
> our wizards in the ivory towers decide...
No problem. I didn't started the naming issue. Anyway, future could
provide a value() function that requires the future to be ready and that
throws if there is no value ;-)
>
>>
>>>
>>>>>
>>>>> 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 don't follows you here. How the fact that expected<T,E> could be
>> default constructible and returning by value or by reference are
>> related.
>
> Assuming this function:
> typename std::decay<T>::type expected<T, E>::value()
>
> with the semantics that it invalidates this, you need some way to
> represent the "invalid" state. This invalid state could also be reused
> when default constructing...
Oh I see it now. However I don't see yet the benefice of invalidate the
value. Why do you want to invalidate the stored value?

>
>>>
>>>> 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...
>> I've changed my mind since I wrote this as my arguments were wrong.
>> Either expected doesn't provide a default constructor or if it does we
>> need to default it to T.
>
> I don't like the default to T{}. It seems arbitrary. I would rather
> see the default ctor vanish altogether and have the user go the extra
> mile when he really needs something like:
> expected<T, E> exp(/* the user decides what to do here, be it T{}, or
> make_unexpected<E>(....);*/);
> {
> // produce expected result here.
> }
> return exp;
>
>
Lastly I'm also for removing the default constructor. Just to note that
the current proposal default to T{} and some one needs to make a
contre-proposal ;-)

Vicente


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