Typed throw functions

So you want to reduce:

catch let error as FooError { ... }

to

catch error as FooError { ... }

or even

catch FooError { ... }

We have that covered: https://github.com/minuscorp/swift-typed-throws/blob/master/SE-XXX%20Typed%20throws.md#library-evolution

1 Like

Something along that line. IMO unannotated catch should be reserved for untyped handling. It is much easier to explain that way, and seems to lead to a more coherent system. Swift types are know to get very long, though.

1 Like

A couple notes—in the section on generic throws parameters, IMO it would be more consistent with Swift's existing generics system for

func foo<T>(_ block: () throws T -> Void) rethrows T

to be a compilation error, rather than raising the error at the call site (i.e, we should require the T: Error constraint). E.g., if this were instead written as:

func foo<T>(_: T.Type, _ block: (() throws T -> Void)?) rethrows T

would it be an error to write:

foo(Int.self, nil)

?

Also, regarding this point:

f({ throw E.failure }) // closure gets inferred to be `() throws E -> Void` so this will compile fine

as I noted in a comment above, this inference of the throws type is not, in general, source compatible. Is the intent that this inference only takes place in the context of a generic throws type? If so, that should be made very explicit.

There's no way that T does not conform to Error because it is annotated after the throws statement, so it is verbose IMO.

It is compatible because it is the block which is being inferred to be E, we were talking about the inference inside the empty catch clause. That function behaves as any other generic function where the type is inferred automatically.

correct, but this can be open for discussion when the pitch is finished next week

Is there precedent in the language for this sort of implicit generic constraint? My mental model for generics right now is that if I supply a concrete type which satisfies the generic constraints, the call will compile. I'm wary about breaking this model. E.g., it feels wrong to me that the following compiles just fine:

func foo<T>(_: T.Type, _: () throws T -> Void) rethrows T { ... } // Compiles fine

but we would get an error here:

func bar<T>(_: T.Type) {
  foo(T.self, { }) // Error!
}

If you're limiting this only to contexts where you are inferring the throws type for a generic throws T parameter, then yes, this is source compatible (since there are no such throws T declarations today, of course).

However, if this inference is meant to be more broad, it is not source compatible:

struct Foo: Error { ... }
struct Bar: Error { ... }
var throwers = [{ throw Foo() }] // Inferred as `Array<() throws -> ()>`, or `Array<() throws Foo -> ()>`?
throwers.append({ throw Bar() }) // Compiles today, error if we infer `throws Foo`
3 Likes

To be clear, I mean that all throw statements becomes typed, and unannotated catch blocks never attempt to infer the error type and use only Error.

1 Like

For instance, Hashable is inferred here:

func singleton<K, V>(_ key: K, _ value: V) -> [K: V] {
  return [key: value]
}

print(singleton(1, 2))

(Personally,I think it's a C++'ism which would've better been left explicit.)

4 Likes

Nice! Looks like my intuition is already broken, then. :slightly_smiling_face:

It seems reasonable that this behavior is carried over to typed throws.

Just read the new proposal document. A function can list only one throw type. So if I had a function that channeled two closures, each with its own error type, what am I supposed to do? I would want to list both in the new function's list. I could hope that the two arguments have the same type, or one is a subtype of the other; otherwise, the new function would need to list either Error or a general throws.

Should you mention function sub-typing. A non-throwing function is a subtype with any throwing function (with the compatible parameters & return-type). A typed-throws function is a subtype of a general throwing function. Two typed-throws functions have a subtype relationship if the corresponding thrown types have a subtype relationship. (If we add support for multiple typed-throws, then the various superset rules apply.)

You either catch both errors in their corresponding catch clause and throw the convenient throwing type of the function or maybe typed function is not appropriate for that function.
This is not for every function. It has its use cases

1 Like

IIRC that is mentioned near Library Evolution section

I don't have time to read this whole thread or read the draft in detail, but it looks like you haven't adopted the design where non-throwing functions are equivalent to throws Never. I strongly suggest you consider going in this direction as it improves composability and addresses the complications around rethrows. Here's an old thread that is contains some relevant discussion.

6 Likes

We never rejected that idea, but it seems more than an implementation detail to me. Also none of the authors encountered issues with rethrows for now, but thank you for the reference, I'll check it out.

Edit: I don't think we want to erase rethrows from the language rn, as it does not impact on the proposal, also, also I don't think we want to make everything throw typed as it does not solve some issues that we have, they add even more edge cases.

This isn't an implementation issue, it's much more than that. It's central to how typed throws fits into he type system.

I'm not suggesting you do that.

You don't need to do that at the syntax level, but in the analysis I've done it simplifies things at the semantic level to interpret the existing syntax in terms of throws Never and throws Error.

7 Likes

Then apologies for not understanding your point in first place. I'll take in consideration how does it fit in the type system, I trust your analysis, we'll add it to the proposal in the grammar section :smile:

1 Like

You're right. It is a non-goal (in my opinion) for Swift's error handling to be as expressive as TypeScript or Java's error handling. Its only goal is to be great for Swift.

On a few other points, yes, closures (and function types in general) should support typed throws. rethrows should not take a type, it should propagate the type from the closure to the callee. This would make sure that:

   try { 
     ... = try thing.map { foo($0) }
   } catch let x {
   }

infers x to FooError if foo is a function that throws only FooError.

-Chris

5 Likes

I agree w/@Chris_Lattner3 that rethrow should just match argument. It typed throws only if the argument is typed throw. The usage of rethrow is only to propagate the throwing anyway.

Now there's some interesting capabilities when we include typed throws, we may be able to restrict the error type:

func foo<T: FooError>(_: () throws FooError -> ()) {...}

But it seems to be a very questionable feature.

Right now, a function can only specify one thrown type, So what does a function that takes multiple closure arguments which may have distinct thrown types do during rethrows? It:

  1. Throws dual types, although the user can't directly spell out such types?
  2. Throws the most-derived common base type (which may be Error)?
  3. Uses the general throw?
  4. Does [1], but we add support user-level spelling of multiple thrown types?

I'm aiming towards [4].

2 Likes

This is what I would expect.

#1 is not possible - you can only specify one error type. I don't understand what you mean with #4.

Related to rethrows, here's an example to consider:

func takesTwo<E, F>(_ e: () throws E -> Void, _ f: () throws F -> Void) throws E -> Void {
  try e()
  do {
    try f()
  } catch _ {
    print("I'm swallowing f's error")
  }
}

This works like rethrows in that when E is Never the call to takesTwo will not be a throwing call. It works better than rethrows in that it is not throwing even when F is Error or some other error type (i.e. not Never). This is an example of why it is important to make non-throwing functions equivalent to throws Never.