[Proposal] Typed throws


(Anton Zhilin) #1

I’ve created a proposal draft, copy-pasting some parts from David Owens’
proposal.
Here it is <https://gist.github.com/Anton3/3e4081da1adbc6e7a7f377987985c289>
.

I had to make one addition, which hasn’t been discussed yet. Look at Multiple
throwing calls
<https://gist.github.com/Anton3/3e4081da1adbc6e7a7f377987985c289#multiple-throwing-calls-in-one-do-block>
section. Is this feature controversal, or it’s fine to keep in the
proposal? Is it feasible from implementation point of view?


Why doesn't Swift have explicit throwables like Java
(Matthew Johnson) #2

Thank you for taking the time to put this proposal together Anton! I really want to see typed throws make it into Swift 4. This will be a very nice feature to have.

I noticed that you included Joe Groff’s idea of replacing `rethrows` by making every function have an error type which is by default `Never` for non-throwing functions and `Error` for throwing functions that do not specify an error type.

I want to urge you to consider updating the proposal to take this direction now rather than later. This is a breaking change which means the longer we wait the harder it is to justify. In fact, I think incorporating the breaking change could increase the chances of it being accepted for Swift 4. Without that it is a purely additive change and those are not being given priority in the Swift 4 release.

···

On Feb 18, 2017, at 6:54 AM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

I’ve created a proposal draft, copy-pasting some parts from David Owens’ proposal.
Here it is <https://gist.github.com/Anton3/3e4081da1adbc6e7a7f377987985c289>.

I had to make one addition, which hasn’t been discussed yet. Look at Multiple throwing calls <https://gist.github.com/Anton3/3e4081da1adbc6e7a7f377987985c289#multiple-throwing-calls-in-one-do-block> section. Is this feature controversal, or it’s fine to keep in the proposal? Is it feasible from implementation point of view?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Martin Waitz) #3

Seconded.
With typed throwing function parameters, it makes a lot of sense to be able to specify the rethrown type, based on the function given as parameter.

Now some bike-shedding:
I’m not really happy with the `throws(Type)` syntax, as it is too close to function parameters.
Why exactly is `throws Type` ambiguous?
The proposal mentions `Type -> Result` as potential thrown type, but functions cannot conform to `Error`.
Maybe we can instruct the parser to just allow simple type names between `throws` and the arrow `->`.

If that is not possible, we should at least try to find some visual hints to separate Error type from function parameters.

E.g. we could use brackets (think of: we are specialising the `throws`):

  func foo() throws<MyError> { … }

— Martin

···

Am 18.02.2017 um 17:37 schrieb Matthew Johnson via swift-evolution <swift-evolution@swift.org>:

Thank you for taking the time to put this proposal together Anton! I really want to see typed throws make it into Swift 4. This will be a very nice feature to have.

I noticed that you included Joe Groff’s idea of replacing `rethrows` by making every function have an error type which is by default `Never` for non-throwing functions and `Error` for throwing functions that do not specify an error type.

I want to urge you to consider updating the proposal to take this direction now rather than later. This is a breaking change which means the longer we wait the harder it is to justify. In fact, I think incorporating the breaking change could increase the chances of it being accepted for Swift 4. Without that it is a purely additive change and those are not being given priority in the Swift 4 release.


(Anton Zhilin) #4

Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not
throw—it would be lost.

Secondly, rethrows allows a function with multiple throwing function
parameters to become non-throwing iff all the arguments are non-throwing.
How would you express it with generics?
In the proposal, in this case you are required to provide a non-generic
supertype of all used error types. This type can’t just turn into Never
automatically.

One solution would be to retain rethrows as an additional attribute.
It would help with semantic information, and resulting error will be able
to turn into Never as a special case—now that we know, that this function
doesn’t throw that error itself.

···

2017-02-19 0:16 GMT+03:00 Martin Waitz <tali@admingilde.org>:

Am 18.02.2017 um 17:37 schrieb Matthew Johnson via swift-evolution < > swift-evolution@swift.org>:

Thank you for taking the time to put this proposal together Anton! I
really want to see typed throws make it into Swift 4. This will be a very
nice feature to have.

I noticed that you included Joe Groff’s idea of replacing `rethrows` by
making every function have an error type which is by default `Never` for
non-throwing functions and `Error` for throwing functions that do not
specify an error type.

I want to urge you to consider updating the proposal to take this
direction now rather than later. This is a breaking change which means the
longer we wait the harder it is to justify. In fact, I think incorporating
the breaking change could increase the chances of it being accepted for
Swift 4. Without that it is a purely additive change and those are not
being given priority in the Swift 4 release.

Seconded.
With typed throwing function parameters, it makes a lot of sense to be
able to specify the rethrown type, based on the function given as parameter.


(Anton Zhilin) #5

Now some bike-shedding:
I’m not really happy with the `throws(Type)` syntax, as it is too close to
function parameters.
Why exactly is `throws Type` ambiguous?
The proposal mentions `Type -> Result` as potential thrown type, but
functions cannot conform to `Error`.

Well, it's expected to change with one of the follow-up proposals.

Maybe we can instruct the parser to just allow simple type names between
`throws` and the arrow `->`.

The ambiguity here is not so "to compiler" as "to human". We don't want
people to spend extra time parsing the declaration.

If that is not possible, we should at least try to find some visual hints

to separate Error type from function parameters.

E.g. we could use brackets (think of: we are specialising the `throws`):

func foo() throws<MyError> { … }

I personally prefer parentheses, because there is precedence of
parametrized attributes. I wonder what others think on angle brackets
option.

···

2017-02-19 0:16 GMT+03:00 Martin Waitz <tali@admingilde.org>:


(Matthew Johnson) #6

Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not throw—it would be lost.

Can you elaborate further on what you mean by this?

Secondly, rethrows allows a function with multiple throwing function parameters to become non-throwing iff all the arguments are non-throwing.
How would you express it with generics?
In the proposal, in this case you are required to provide a non-generic supertype of all used error types. This type can’t just turn into Never automatically.

This is a very good question. I’m curious if Joe has a solution that doesn’t require introducing any additional features into Swift.

The answer I would like to see (that won’t happen in Swift 4) is to adopt some of the ideas in my value subtyping manifesto, including structural unions (that *do not* expose any members, even if there are members that all types have in common). All you would be able to do with such a union is convert from one of its constituent types to the union type and cast back down to one of the constituent types.

Having unions like that would very elegantly solve the problem of a function that needs to throw more than one type. It also covers the case you bring up: `Never | Never | Never` would be the exact same type as `Never`. You would never write out `Never | Never | Never` of course, but in a generic context it’s important that we be able to spell it that way and have it collapse to a simple unordered set of its constituent types.

Use of these structural union types would be discouraged in most cases, but they would be very powerful and useful in specialized contexts. Error handling is one of those contexts. Another is at the boundary of a system when you need to accept a heterogenous collection of a small, fixed set of types (as is discussed in the thread about open and public protocols).

One solution would be to retain rethrows as an additional attribute.
It would help with semantic information, and resulting error will be able to turn into Never as a special case—now that we know, that this function doesn’t throw that error itself.

I'm curious to hear Joe’s thought on this. It’s possible this would be a necessary bridge solution until we have something more permanent as I described above.

···

On Feb 19, 2017, at 1:32 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

2017-02-19 0:16 GMT+03:00 Martin Waitz <tali@admingilde.org <mailto:tali@admingilde.org>>:

Am 18.02.2017 um 17:37 schrieb Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>>:

Thank you for taking the time to put this proposal together Anton! I really want to see typed throws make it into Swift 4. This will be a very nice feature to have.

I noticed that you included Joe Groff’s idea of replacing `rethrows` by making every function have an error type which is by default `Never` for non-throwing functions and `Error` for throwing functions that do not specify an error type.

I want to urge you to consider updating the proposal to take this direction now rather than later. This is a breaking change which means the longer we wait the harder it is to justify. In fact, I think incorporating the breaking change could increase the chances of it being accepted for Swift 4. Without that it is a purely additive change and those are not being given priority in the Swift 4 release.

Seconded.
With typed throwing function parameters, it makes a lot of sense to be able to specify the rethrown type, based on the function given as parameter.


(Anton Zhilin) #7

protocol Default { init() }

func exec(f: () throws -> Void) rethrows
{
    try f()
    throw MyError() // error because of rethrows
}

func exec<E>(f: () throws(E) -> Void) throws(E)
     where E: Error & Default
{
    try f()
    throw E() // okay
}

···

2017-02-19 22:59 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 19, 2017, at 1:32 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not
throw—it would be lost.

Can you elaborate further on what you mean by this?


(Colin Barrett) #8

Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not
throw—it would be lost.

That's not true. Parametric polymorphism guarantees that rethrows and
polymorphic throw are the same.

For example, you can prove that as a consequence of parametricity that
there is only one (pure) function in the of the set of all functions with
the type ``forall A. A -> A'' and furthermore that it is the identity
function.

The intuition behind this is that you (meaning the fiction; imagine being a
function!) cannot construct your own value of "A" since you don't have any
information about what "A" is. The only place to get an "A" is from your
argument.

Secondly, rethrows allows a function with multiple throwing function

···

On Sun, Feb 19, 2017 at 2:34 PM Anton Zhilin via swift-evolution < swift-evolution@swift.org> wrote:

parameters to become non-throwing iff all the arguments are non-throwing.
How would you express it with generics?
In the proposal, in this case you are required to provide a non-generic
supertype of all used error types. This type can’t just turn into Never
automatically.

One solution would be to retain rethrows as an additional attribute.
It would help with semantic information, and resulting error will be able
to turn into Never as a special case—now that we know, that this function
doesn’t throw that error itself.

2017-02-19 0:16 GMT+03:00 Martin Waitz <tali@admingilde.org>:

Am 18.02.2017 um 17:37 schrieb Matthew Johnson via swift-evolution < > swift-evolution@swift.org>:

Thank you for taking the time to put this proposal together Anton! I
really want to see typed throws make it into Swift 4. This will be a very
nice feature to have.

I noticed that you included Joe Groff’s idea of replacing `rethrows` by
making every function have an error type which is by default `Never` for
non-throwing functions and `Error` for throwing functions that do not
specify an error type.

I want to urge you to consider updating the proposal to take this
direction now rather than later. This is a breaking change which means the
longer we wait the harder it is to justify. In fact, I think incorporating
the breaking change could increase the chances of it being accepted for
Swift 4. Without that it is a purely additive change and those are not
being given priority in the Swift 4 release.

Seconded.
With typed throwing function parameters, it makes a lot of sense to be
able to specify the rethrown type, based on the function given as parameter.


_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #9

Thanks. There is nothing wrong with this at all. Your second `exec` would not accept a non-throwing function because `Never` cannot conform to `Default`. If it didn’t include the `Default` constraint it would not be able to `throw E()`.

If you remove the `Default` constraint and change `throws(E)` to `throws`, and throw `MyError()` in place of `E()` in both places then it behaves exactly as the first example. We don’t lose any expressivity at all.

This is actually an example of a strength of Joe’s suggestion: the second `exec` is able to throw an error of a type that matches the error that might be thrown by the calling argument `f`. I’m not sure of where this might be useful but it is definitely not possible with `rethrows` while it is possible with Joe’s proposal. We have more flexibility in API design under Joe’s proposal.

You did make a great point about coalescing error types from several different function arguments. That’s an edge case, but the inability to coalesce is indeed a problem that probably needs to be addressed by the typed throws proposal in one way or another (if we don’t go with Joe’s suggestion we would need to specify how `rethrows` behaves in cases like this in some detail).

···

On Feb 19, 2017, at 2:16 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

2017-02-19 22:59 GMT+03:00 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>>:

On Feb 19, 2017, at 1:32 PM, Anton Zhilin <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:

Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not throw—it would be lost.

Can you elaborate further on what you mean by this?

protocol Default { init() }

func exec(f: () throws -> Void) rethrows
{
    try f()
    throw MyError() // error because of rethrows
}

func exec<E>(f: () throws(E) -> Void) throws(E)
     where E: Error & Default
{
    try f()
    throw E() // okay
}


(Anton Zhilin) #10

Thanks. There is nothing wrong with this at all. Your second `exec` would

not accept a non-throwing function because `Never` cannot conform to
`Default`. If it didn’t include the `Default` constraint it would not be
able to `throw E()`.

But that raises another concern. In a previous discussion, it was taken for
granted that Never should conform to all protocols, 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.

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.
Don’t misunderstand me—I love the idea of Never replacing rethrows. Just
want to make sure we don’t lose anything important by the way.

If you remove the `Default` constraint and change `throws(E)` to `throws`,

and throw `MyError()` in place of `E()` in both places then it behaves
exactly as the first example. We don’t lose any expressivity at all.

This is actually an example of a strength of Joe’s suggestion: the second
`exec` is able to throw an error of a type that matches the error that
might be thrown by the calling argument `f`. I’m not sure of where this
might be useful but it is definitely not possible with `rethrows` while it
is possible with Joe’s proposal. We have more flexibility in API design
under Joe’s proposal.

Nice. Whether we can throw the error ourselves depends on our ability to
create the error.

You did make a great point about coalescing error types from several

different function arguments. That’s an edge case, but the inability to
coalesce is indeed a problem that probably needs to be addressed by the
typed throws proposal in one way or another (if we don’t go with Joe’s
suggestion we would need to specify how `rethrows` behaves in cases like
this in some detail).

···

2017-02-19 23:29 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:


(Anton Zhilin) #11

How about this:

func bypassRethrows<E: Error>(_ f: () throws(E) -> ()) throws(E) {
    if let e = (MyError() as Error) as? E {
        throw e
    }
}

I obviously can’t “test” this right now, but should work?

···

2017-02-22 3:42 GMT+03:00 Colin Barrett <colin@springsandstruts.com>:

On Sun, Feb 19, 2017 at 2:34 PM Anton Zhilin via swift-evolution < > swift-evolution@swift.org> wrote:

Now that I think about it, generic throws does not exactly cover rethrows
.
Firstly, rethrows has semantic information that function itself does not
throw—it would be lost.

That's not true. Parametric polymorphism guarantees that rethrows and
polymorphic throw are the same.

For example, you can prove that as a consequence of parametricity that
there is only one (pure) function in the of the set of all functions with
the type ``forall A. A -> A'' and furthermore that it is the identity
function.

The intuition behind this is that you (meaning the fiction; imagine being
a function!) cannot construct your own value of "A" since you don't have
any information about what "A" is. The only place to get an "A" is from
your argument.


(Matthew Johnson) #12

2017-02-19 23:29 GMT+03:00 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>>:

Thanks. There is nothing wrong with this at all. Your second `exec` would not accept a non-throwing function because `Never` cannot conform to `Default`. If it didn’t include the `Default` constraint it would not be able to `throw E()`.

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.

, 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.

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.

···

On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

Don’t misunderstand me—I love the idea of Never replacing rethrows. Just want to make sure we don’t lose anything important by the way.

If you remove the `Default` constraint and change `throws(E)` to `throws`, and throw `MyError()` in place of `E()` in both places then it behaves exactly as the first example. We don’t lose any expressivity at all.

This is actually an example of a strength of Joe’s suggestion: the second `exec` is able to throw an error of a type that matches the error that might be thrown by the calling argument `f`. I’m not sure of where this might be useful but it is definitely not possible with `rethrows` while it is possible with Joe’s proposal. We have more flexibility in API design under Joe’s proposal.

Nice. Whether we can throw the error ourselves depends on our ability to create the error.

You did make a great point about coalescing error types from several different function arguments. That’s an edge case, but the inability to coalesce is indeed a problem that probably needs to be addressed by the typed throws proposal in one way or another (if we don’t go with Joe’s suggestion we would need to specify how `rethrows` behaves in cases like this in some detail).


(Anton Zhilin) #13

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.

, 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.

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 :slight_smile:

···

2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:


(Matthew Johnson) #14

How about this:

func bypassRethrows<E: Error>(_ f: () throws(E) -> ()) throws(E) {
    if let e = (MyError() as Error) as? E {
        throw e
    }
}
I obviously can’t “test” this right now, but should work?

No because there are many types that conform to `Error` but are not `E` and the signature says you only throw `E`. You have no way to get an instance of `E` except by catching one thrown by `f` because `Error` does not have any initializers and you don’t have any visibility to anything that provides an `E` in any way except when `f` throws.

I am hoping to have time to write up my analysis of generic rethrowing functions later today. There are some very interesting tradeoffs we need to make.

···

On Feb 22, 2017, at 7:10 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

2017-02-22 3:42 GMT+03:00 Colin Barrett <colin@springsandstruts.com <mailto:colin@springsandstruts.com>>:

On Sun, Feb 19, 2017 at 2:34 PM Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Now that I think about it, generic throws does not exactly cover rethrows.
Firstly, rethrows has semantic information that function itself does not throw—it would be lost.

That's not true. Parametric polymorphism guarantees that rethrows and polymorphic throw are the same.

For example, you can prove that as a consequence of parametricity that there is only one (pure) function in the of the set of all functions with the type ``forall A. A -> A'' and furthermore that it is the identity function.

The intuition behind this is that you (meaning the fiction; imagine being a function!) cannot construct your own value of "A" since you don't have any information about what "A" is. The only place to get an "A" is from your argument.


(Anton Zhilin) #15

I understand how parametric polymorphism works *in Haskell*. But we talk
about Swift, and there *is* a way to get an instance of E. I’ll explain it
another way:

func bypassRethrows<E: Error>(_ f: () throws(E) -> ()) throws(E) {
    let error: Error = MyError() // create an instance of `MyError`
    if MyError.self is E.Type { // in case `E` happens to be `MyError`
        let e: E = error as! E // then we've actually created an
instance of `E`, and we can downcast safely
        throw e // voila, acquired an instance of `E`
    }
}
let f: () throws MyError -> () = { }
try bypassRethrows(f) // actually throws `MyError`,
without ever calling `f`

What line here seems impossible?

No because there are many types that conform to `Error` but are not `E` and

the signature says you only throw `E`. You have no way to get an instance
of `E` except by catching one thrown by `f` because `Error` does not have
any initializers and you don’t have any visibility to anything that
provides an `E` in any way except when `f` throws.

I am hoping to have time to write up my analysis of generic rethrowing
functions later today. There are some very interesting tradeoffs we need
to make.

···

2017-02-22 18:39 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:


(Matthew Johnson) #16

2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>>:

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 :slight_smile:

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.

···

On Feb 20, 2017, at 11:14 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:


(Anton Zhilin) #17

I messed up with links a little bit, duplicating one of them. Here
<http://discourse.natecook.com/t/marking-sort-and-sorted-with-rethrows/1764>
is another one.


(Anton Zhilin) #18

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@anandabits.com>:

2017-02-21 1:21 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 20, 2017, at 11:14 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@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 :slight_smile:

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.


(Anton Zhilin) #19

As a follow-up, here is code that you can test now. It demonstrates the
same, but with normal function result.

func createArbitrary<T>(usingGenerator f: () -> (T)) -> T {
    let any: Any = 42 as Any
    if Int.self is T.Type {
        let t = any as! T
        return t
    }
    return f()
}
print(createArbitrary(usingGenerator: { return 5 })) //=> 42

And yes, nobody should do these tricks in production code :stuck_out_tongue:

I understand how parametric polymorphism works *in Haskell*. But we talk

about Swift, and there *is* a way to get an instance of E. I’ll explain
it another way:

func bypassRethrows<E: Error>(_ f: () throws(E) -> ()) throws(E) {
    let error: Error = MyError() // create an instance of `MyError`
    if MyError.self is E.Type { // in case `E` happens to be `MyError`
        let e: E = error as! E // then we've actually created an instance of `E`, and we can downcast safely
        throw e // voila, acquired an instance of `E`
    }
}
let f: () throws MyError -> () = { }
try bypassRethrows(f) // actually throws `MyError`, without ever calling `f`

What line here seems impossible?

···

2017-02-22 21:15 GMT+03:00 Anton Zhilin <antonyzhilin@gmail.com>:


(Matthew Johnson) #20

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.

I've been continuing to think through these generic rethrowing functions and have identified a problem I think we want to address before submitting a proposal for review. Please hold off until I have a chance to write a post describing the problem and laying out our options.

···

Sent from my iPhone

On Feb 20, 2017, at 5:51 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:
2017-02-21 1:21 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

2017-02-21 1:21 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 20, 2017, at 11:14 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

2017-02-20 18:23 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 20, 2017, at 3:58 AM, Anton Zhilin <antonyzhilin@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 is the discussion where the idea of “empty” type originated.
Some messages on the topic ended up being there.

This is the earliest mention of usage of this empty type for rethrows I could find.
Some related messages are here 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 :slight_smile:

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.