[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