[swift-evolution] [Proposal] Typed throws
Anton Zhilin
antonyzhilin at gmail.com
Mon Feb 20 17:51:19 CST 2017
2017-02-21 1:21 GMT+03:00 Matthew Johnson <matthew at anandabits.com>:
>
> Thanks for the links. I scanned through them somewhat quickly and didn’t
> see anything that specifically said `Never` should conform to all
> protocols. Did you see that specifically? I only saw mentions of it being
> a bottom type and therefore a subtype of all types, which I think is a bit
> different.
>
> I think a big part of the confusion here revolves around the distinction
> between a type `T` being a subtype of another type `U` and `Type<T>` being
> a subtype of `Type<U>` (using the syntax in your metatype refactoring
> proposal). I’m not an expert in this area, but I suspect that `Never` can
> be a subtype of all existential types but without requiring it to actually
> *conform* to all protocols. Any non-instance protocol requirements are not
> available on existentials (afaik).
>
I didn't fully understand about metatypes, but otherwise yes indeed.
Yes, I understood the example and it’s a good one. What I’m wondering is
> what benefit you actually get from this. There are two places where this
> default initializer could be used:
>
> 1. In `seq` itself. But it seems highly dubious to throw an error you
> know nothing about. Why does `seq` need the ability to construct an error
> of the same type as a function given to it without knowing anything more
> about that error type. Is there a use case for this?
> 2. In callers of `seq`. Why would the caller care if the error type that
> `seq` can throw has a default initializer? Is there a use case for this?
>
> In other words, why do you want to specify that the type of error that
> might be thrown must have a default initializer? I can’t think of any
> possible circumstance where this would be valuable.
>
> The same question can be asked of any other static requirements. What are
> the use cases? These seem highly theoretical to me. Maybe I’m missing
> something, but if so I would like to see an example of how it is *used*,
> not just how you would need to write an extra overload without `rethrows`.
>
Seems highly theoretical to me as well.
There is a potentially more practical benefit of keeping rethrows. If a
> function is declared with `rethrows` we know that the function itself does
> not throw. It only throws if one of its arguments throw when it invokes
> them. This is a subtle but important difference. For example, users
> calling a rethrowing function know that *they* have control over whether or
> not the call *actually* throws. The caller might pass a couple of
> functions that *can* throw but in this particular case are known not to
> throw. That could influence how the caller handles errors in the
> surrounding scope.
>
Agreed. Now I lean towards leaving the proposal as is.
2017-02-21 1:21 GMT+03:00 Matthew Johnson <matthew at anandabits.com>:
>
> On Feb 20, 2017, at 11:14 AM, Anton Zhilin <antonyzhilin at gmail.com> wrote:
>
> 2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew at anandabits.com>:
>
>
>
>> On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin at gmail.com> wrote:
>>
>> But that raises another concern. In a previous discussion, it was taken
>> for granted that Never should conform to all protocols
>>
>> Do you have a pointer to this discussion? I must have missed it.
>>
>
>
> Here
> <http://discourse.natecook.com/t/idea-change-noreturn-func-f-to-func-f-noreturn/1000>
> is the discussion where the idea of “empty” type originated.
> Some messages on the topic ended up being there
> <http://discourse.natecook.com/t/idea-repurpose-void/1406>.
>
> This <http://discourse.natecook.com/t/idea-repurpose-void/1406> is the
> earliest mention of usage of this empty type for rethrows I could find.
> Some related messages are here
> <http://discourse.natecook.com/t/draft-change-noreturn-to-unconstructible-return-type/1765/16>
> as well.
>
> We called this type NoReturn and meant it to be the *bottom type*, i.e.
> subtype of all types, meaning that if you have an instance of NoReturn—which
> can only happen in unreachable sections of code—then you can convert it to
> any type. It should have worked like this:
>
> func fatalError() -> Never
>
> func divide(a: Int, b: Int) -> Int {
> if b == 0 {
> let n: Never = fatalError()
> return n as Int
> }
> return a / b
> }
>
> I pushed the idea of replacing rethrows with Never, inspired by Haskell.
> Although Haskell doesn’t have static function requirements and initializer
> requirements.
>
>
> Thanks for the links. I scanned through them somewhat quickly and didn’t
> see anything that specifically said `Never` should conform to all
> protocols. Did you see that specifically? I only saw mentions of it being
> a bottom type and therefore a subtype of all types, which I think is a bit
> different.
>
> I think a big part of the confusion here revolves around the distinction
> between a type `T` being a subtype of another type `U` and `Type<T>` being
> a subtype of `Type<U>` (using the syntax in your metatype refactoring
> proposal). I’m not an expert in this area, but I suspect that `Never` can
> be a subtype of all existential types but without requiring it to actually
> *conform* to all protocols. Any non-instance protocol requirements are not
> available on existentials (afaik).
>
>
>
> , because if one obtains an instance of Never (and they won’t), then
>> everything is possible. But now we say that Never can’t conform to
>> Default, because this would break its very invariant. Also it can’t
>> conform to any protocol with static members or initializers.
>>
>> It seems highly problematic to me to say that never conforms to any
>> protocol with non-instance requirements.
>>
>
>
> Here is an example with instance requirements only:
>
> protocol MakesPizza {
> func cook() -> Pizza
> }
> extension Never : MakesPizza {
> func cook() -> Pizza {
> // this method will never be called anyway
> burnThisComputer()
> }
> }
>
> let maestroLaPizza = isHeAtWork ? validMaestro : (fatalError("something went wrong") as MakesPizza)
> maestroLaPizza.cook()
>
> In this way, Never can conform to any protocol with only instance
> requirements.
>
> Sure.
>
>
>
> But then basically, Never trick can’t be used when we request anything
>> more than Error from generic error type (with static members or
>> initializers). So this approach turns out to be more limiting than
>> rethrows.
>>
>> Can you elaborate here? If you require a function to throw an error type
>> that has non-instance requirements then you would necessarily be
>> restricting callers to provide a throwing function. It is not possible to
>> express such a function with `rethrows`. You can’t talk about the error
>> type at all. If you could talk about the error type and were able to
>> constrain it in this way `rethrows` would necessarily have to exhibit the
>> same behavior as the generic version. The behavior arises out of the
>> constraint you are applying, not the mechanism by which you forward the
>> type.
>>
>
>
> With rethrows approach:
>
> protocol BaseError : Error {
> init()
> }
>
> func seq<E1, E2>(f: () throws(E1) -> (), g: () throws(E2) -> ()) rethrows(BaseError)
> where E1: BaseError, E2: BaseError { ... }
>
> With Never approach, we have to create two separate functions for the
> same effect, because Never does not fit in BaseError:
>
> func seq<E1, E2>(f: () throws(E1) -> (), g: () throws(E2) -> ()) throws(BaseError)
> where E1: BaseError, E2: BaseError {
> // It never actually throws E1() or E2() itself, but this fact can't be reflected in the signature
> }
>
> func seq(f: () -> (), g: () -> ()) {
> // repeat the body
> }
>
> That’s where loss of information (which I meantioned earlier) hurts: we
> can’t apply magic and say “if E1 and E2 are Never then seq does not
> throw. Because it *can* throw anyway.
>
> Well, I’m just repeating myself, at least I gave a bit more complete
> example :)
>
>
>
> Yes, I understood the example and it’s a good one. What I’m wondering is
> what benefit you actually get from this. There are two places where this
> default initializer could be used:
>
> 1. In `seq` itself. But it seems highly dubious to throw an error you
> know nothing about. Why does `seq` need the ability to construct an error
> of the same type as a function given to it without knowing anything more
> about that error type. Is there a use case for this?
> 2. In callers of `seq`. Why would the caller care if the error type that
> `seq` can throw has a default initializer? Is there a use case for this?
>
> In other words, why do you want to specify that the type of error that
> might be thrown must have a default initializer? I can’t think of any
> possible circumstance where this would be valuable.
>
> The same question can be asked of any other static requirements. What are
> the use cases? These seem highly theoretical to me. Maybe I’m missing
> something, but if so I would like to see an example of how it is *used*,
> not just how you would need to write an extra overload without `rethrows`.
>
> There is a potentially more practical benefit of keeping rethrows. If a
> function is declared with `rethrows` we know that the function itself does
> not throw. It only throws if one of its arguments throw when it invokes
> them. This is a subtle but important difference. For example, users
> calling a rethrowing function know that *they* have control over whether or
> not the call *actually* throws. The caller might pass a couple of
> functions that *can* throw but in this particular case are known not to
> throw. That could influence how the caller handles errors in the
> surrounding scope.
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170221/9e0ef3ef/attachment.html>
More information about the swift-evolution
mailing list