Typed throws

Bikeshedding alarm: Could rethrows be replaced through throws?(E)?

public enum MyLibraryError: Error {
    case errorProcessingHashedData(baseError: Error)
}

public func withHashedData<ReturnValue>(
_ input: Data, _ body: (SHA256Digest) throws -> ReturnValue
) throws?(MyLibraryError) -> ReturnValue {
    let hashed = SHA256.hash(data: input)
    do {
        return try body(hashed)
    } catch {
        throw MyLibraryError.errorProcessingHashedData(baseError: error)
    }
}

It would preserve the throwing if the closure thrower, but would expose the custom error directly. If the closure is non throwing, it won't throw its own type.

In other words this is basically a typed rethrows.

I really like that we go into interesting detail discussions, but just to give this discussion direction and structure, can we first clear up some determining factors?

Can someone explain to me, what changes to the language are in general allowed at all at this stage of the language?

  • Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess. Concentrate on section "Source Compatibility" first as I already mentioned.
  • Is ABI stability required no matter what?

To me this is important, because it determines the options we have at all and we can fail on nasty details fast here without needing a discussion for the other things (though I really like the ideas you come up with :+1:).

thx :slight_smile:

The most recent answer to your question is the "Principles for Evolution Proposal Evaluation" chapter of Principles for Trailing Closure Evolution Proposals

Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess.

Source compatibility is the first of those principles, so I agree with your conclusion. This pitch won't easily turn into a proposal.

Is ABI stability required no matter what?

Of course it is. You can't break existing devices. But you can evolve the language so that typed throws is only available from a specific Swift version and specific system version (compare with some View which is only available on some version of the systems that rely on ABI stability).

2 Likes

Exactly what I was looking for. Thx!

This was my conclusion along the last days writing on the pitch. Bringing more information into the error types making them more specific in combination with type inference leads to a situation where error types change automatically in existing code (getting more specific). So as long as we are not auto converting source code (which I guess is not an option anymore) to explicitly declare the existing errors to be of type Swift.Error, I see no way to bring this into the language.

Other options?

1 Like

Well, start to see what happens when you do not automatically change error types in existing source code?

Maybe you'll end up with a perfectly viable language with useful new features, and that will be cool. Maybe you'll end up in a dead-end, but the community will have a better sense of what's to fix.

Anyway, the goal is not to tame the language so that it is like you want it to be. There are many flavors of "typed throws", and the game is to find the one that suits Swift the best.

2 Likes

And that's the whole purpose of this thread. There're some ideal cases where typed throws could be applied into the language and be useful, but the break source compat. The base idea we're proposing does not break source compat at first analysis, but it is more limited about what could it do breaking it.
I'm eager to see more ideas and helping us polishing the raw edges of the pitch to make it into a proposal soon.


Having some help on implementing the basic behavior (syntax) it would be very useful for us to see what can and what can't be done. Where should I start? On swift-syntax?

I wan't to reduce potential risks as early as possible. So to be concrete and keep my motivation going, I have no solution keeping source compatibility for this

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`

If someone has, please share! :slight_smile:

You answered your own question:

// Compiles today, error if we infer throws Foo

So don't infer throws Foo, and infer throws Error instead. This will preserve source compatibility.

When one wants an array of functions throwing Foo, they'll write:

var throwers: [() throws Foo -> ()] = [{ throw Foo() }]
3 Likes

For me, I think I told you so. throws functions should never be inferred to be the typed throw but the base throws in collections (array, set), unless the type is specified as follows:

struct Foo: Error { ... }
struct Bar: Error { ... }
var throwers: [() throws Foo -> Void] = [{ throw Foo() }]
throwers.append({ throw Bar() }) // error: type mismatch
1 Like

To summarize: as of today we have two different cases of rethrows.

1:

func foo(_ bar: () throws -> ()) rethrows {
    try bar()
}

2:

func foo(_ bar: () throws -> ()) rethrows {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

Both of these should continue to work exactly as they are now, without any source code changes required.

Maybe I'm overseeing something, but I will just write my idea nonetheless.


If we had a typed throws, we would need a typed rethrows as well.

Our two cases, that we have today would work exactly the same, as they worked in the past. I repeat them here with the implicit types in comments.

1:

func foo(_ bar: () throws /* Error */ -> ()) rethrows /* Error */ {
    try bar()
}

2:

func foo(_ bar: () throws /* Error */ -> ()) rethrows /* Error */ {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

AFAIUI, this is the pre-typed-throws-behaviour.

Now we want the same but with typed Errors as well. So we have the following additional cases:

3 (typed version of (1)):

func foo<E>(_ bar: () throws E -> ()) rethrows E {
    try bar()
}

// If every function implicitly threw Never, we could write
func foo<E>(_ bar: () throws E -> ()) throws E {
    try bar()
}
// which would semantically be the same

4 (typed version of (2)):

func foo<E>(_ bar: () throws E -> ()) rethrows CustomError {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

I cannot think of a reason ATM, why this should not work.

6 Likes

I don't see a reason why your examples with typed version of rethrows should not be source compatible :thinking:

And If that works, we would have another choice:
Would this:

func foo<E>(_ bar: () throws E -> ()) rethrows { … }

be:
A: equivalent to:

func foo<E>(_ bar: () throws E -> ()) rethrows Error { … }

B: equivalent to:

func foo<E>(_ bar: () throws E -> ()) rethrows E { … }

C: or invalid.

1 Like

Ok right. But without my knowledge about the history of Swift, I would be surprised about

let some /* : () throws -> () */ = { throw FooError.foo }

not being

let some /* : () throws FooError -> () */ = { throw FooError.foo }

Isn't it somehow the default to infer the most specific type possible? :thinking:

But it's good to have this option.

I think it would be feasible to have a rule of the form:

func foo<T>(_: () throws T -> ()) { ... }
let _ = { throw Foo.error } // inferred as '() throws -> ()'
foo({ throws Foo.error }) // inferred as '() throws Foo -> ()'

That is, when there's no context we default to Error, but if there's a generic parameter to bind the throws type to, we allow more specific inference.

It also seems fairly likely to me that the source break wouldn't be that large (unless there's a more common use-case I haven't thought of?). It might make sense to tighten up the inference behavior as a change in Swift 6, where the Swift 5 compatibility mode would continue to infer the throws type as Error.

5 Likes

How would that behave in a do block like

do {
    throw Foo.error // statement infererred as Error instead of Foo?
} catch let error as Foo {
    // would that catch?
}
// can we still check exhaustiveness?

How is catch working internally? Does it need the inferred specific type at this point?

The current behavior tries to match the closest match possible available, so it would go through Foo

If we can show that this is managable for Swift users, that would be clean and consistent. <3

To make it more accessible for everyone on the forum to participate in the discussion, we try to provide an up-to-date excerpt of the discussion going on targeting the main topics we see so far. If you feel that something is missing, let us know. Remember it's an excerpt, if you are really into it, read the whole thread. :smiley:

1 Like

I would add that, to make clear, that the current code keeps compiling.

EDIT:

Sorry, this should probably not be in this thread...

2 Likes

No problem. Thx for that input. PR is open.

So choosing func foo<E: Error>(fn: () throws E -> Void) rethrows E I can only think of the issue with inferred throwing closures like

`func wrap<E: Error>(fn: () throws E -> Void) rethrows E

wrap({ throw Foo() })
// before: () throws Error -> ()
// after: () throws Foo -> ()

But this is an issue for itself on the topic list. But we may can't use this rule then

ABI stability can be an issue as @John_McCall pointed out with erased. Do we need to incorporate that?