[swift-evolution] [Proposal] Typed throws

Matthew Johnson matthew at anandabits.com
Mon Feb 20 16:21:39 CST 2017


> 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 <mailto:matthew at anandabits.com>>:
> 
> 
> 
> 
>> On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin at gmail.com <mailto: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/20170220/a7388dd5/attachment.html>


More information about the swift-evolution mailing list