Typed throws

func generic<E1: Error, E2: Error>
(_ function1: () throws E1 ->  Void,
 _ function2: () throws E2 ->  Void)
 throws (E1|E2|E3)? 
{
    try function1()
    do
    {
      try function2()
    }
    catch //should be E2 here
    {
      if (cond) { throw E3() //E3:Error too}
    }
}

E1 or E2 or E3 which Error type should be thrown out?

This thread is getting overly long that people stops reading the earlier comments before replying (not pointing fingers at anyone). A few times already that they rehash the questions about current rethrows semantic, or propose the exact same solution. It doesn’t help either the each rehashing is about the same length.

2 Likes

It’s definitely not the language’s problem. In my opinion, the whole concept of rethrows, that has lead people to believe that this was the language’s problem to solve, was a quick way of mitigating a source of boilerplate caused by a lacking language feature.

In your scenario, if the API author decided that it’s reasonable for a single function to re-throw two different error types, then clearly it’s their problem to come up with a single error representation that can somehow encapsulate Either<First, Second> of them (pun intended).

In other words, your question is essentially the same as asking:

func foo(isTrue: Bool) -> ??? {
    let some: Int = 42
    let other: String = “42”
    if isTrue {
        return some
    } else {
        return other
    }

What should the compiler understand ??? to be?
The obvious answer, in my opinion, is: “If you think it’s reasonable to mix the unmixable, then it’s your problem to figure out how to actually do the mixing”. The most trivial way to do it would be to find the closes common supertype and mindlessly use that (which in the most generic cases is Any, or, in the context of throwing, Error). But again, automatically making API decisions on behalf of the API author is a bad idea, so the compiler should do absolutely nothing to help come up with an error type.

Edit:

Purely for short-term compatibility sake, rethrows should type-erase any non-Never error type to Error and leave Never alone. This way, rethrows gets to retain its exact behavior. If someone doesn’t want to have their error type lost, then they shouldn’t use a deprecated language feature in the first place.

Perhaps we should build up an error throwing manifesto with an FAQ-style summary of all the points that were made in this thread to avoid any further rehashing

I never want to encounter code like this.

An API which restricts the types of errors that closure parameters are allowed to throw, is diametrically opposed to what I want in an error-handling system.

If an unhandled error occurs at runtime, then the program should crash. It should not fail to compile because such an error might occur—the entire point of runtime error-handling is to recognize that such errors indeed might occur at runtime.

It’s one thing to say that an API can be marked as only ever throwing one particular type of error, so that catch clauses can be simple and exhaustive. I’m not convinced it’s valuable enough to warrant a change to the language and the additional complexity it brings, but it’s at least understandable.

However, restricting the permitted error types of an input parameter is purely anti-ergonomic. I do not see any benefit from that.

If I want to call yourThing with a closure that can throw MyError, that should just work. If the error is caught somewhere (like a catch in my own code), then it’s fine. And if it goes uncaught, then the program crashes just as it should.

That is how error handling works today, and should continue to work in the future.

The language should not prevent me from passing in that closure. The only effect of such a restriction would be an increased amount of boilerplate and glue code that must be written by every user of the API.

I am strongly opposed to letting API authors restrict the types of errors that can be thrown by closure parameters.

2 Likes

There are linked in the first post different sources of information about this.

Really :heart_eyes: the idea but I think it should be optional.

Yes and 6 clicks on the discussion document are far too less compared to the amount of people discussing about the topics mentioned in this document. We can further develop it into a FAQ, but can we please (and I'm really bored of repeating this) first come up with potential solutions for the two issues that are mentioned in the discussion document: https://github.com/minuscorp/swift-typed-throws/blob/master/Typed%20throws%20discussion.md.

Otherwise we can also start two new threads where we discuss these two topics separately, but I don't see any progress on this since days. It is just repeating the same stuff again and again, which may have value for the community as a whole, so that the idea spreads wider, but it's not efficient.

Entering this discussion without even reading the proposal or the discussion document is disrespectful to the people that put in a lot of thought and work over the last weeks from my point of view. It's not needed to read the whole thread. We have a summary for you. This is the idea of the discussion document where we link to the posts in the thread. We want that everyone on the forum has the opportunity to participate in the discussion, but reading these two documents first should be mandatory.

1 Like

Second. If no one can tell me how decisions are made (and by which people) in this evolution process, I don't see how I should be able to bring this idea further to reality.

@minuscorp and some fellows are taking the approach of implementing a solution as a proof of concept, which is reasonable.

From my side I want to have the conceptional part solved as a whole first. We can divide things up into multiple proposals (@Chris_Lattner3) and even bring things into the language step by step, but I think we should have at least a common shared vision for this feature as a whole that is backed by conceptual soundness and the backing of Swift decision makers before starting an implementation (even if it's just a proof of concept) with that complexity.

1 Like

I’m not sure if we need to involve decision makers just yet. I don’t feel like we even have a sound system at this point.

Also, the style of the evolution is generally more about not painting ourselves into the corner, that each step can still go either way, than to have a solid 5 proposal plans ahead.

IMO, the minimum viable product should just be

And also

  • The type and interpretation of throw statements

While making sure that a few potential error propagation is congruent with the design.

Mhh, I see more of a risk of taking irreversible paths for an approach that does not take the big picture into account. Some things are hard to bring into a language later (e.g. Optional in Java). For example if sum types would still be an option, we could not introduce them easily later. They would be a substantial part of a new rethrows. So I see no problem with evaluating ideas a little bit further ahead into the future if that reduces risks.

That's exactly what I mean by why we need some decision process. Your opinion is that this would be enough for a minimum viable feature, but I'm not that convinced. Others may share your or my opinion or another one. All these opinions have their valid arguments. And now? What to do next? We are stuck. So we wait for coincidence that evolution can occur? (That's not pointed at you, I just want to express my concerns with this decision process).

That’s why I said not to paint ourselves into the corner. We do consider future proposal(s), but not to the level that everything is polished and shiny. Especially when we aren’t even guaranteed the first step.

We kind of are. Even if the core team agree with the problem statement and the approach, and I’m not saying that they currently do (I’m not even sure that I do), someone needs to implement it.

Also, most of these decisions are made by the proposers unless something is simply infeasible, which is usually pointed out very early in the pitch phase. They are usually pretty free, while of course are encouraged to take feedback into consideration.

As the one, who suggested Solution 1 to the rethrows issue, I want to make clear, why I suggested it.

the key question with rethrows, which is whether the signature of A below is equivalent to that of B or that of C

// A
func foo(fn: () throws -> Void) rethrows

// B
func foo<erased E: Error>(fn: () throws E -> Void) rethrows E

// C
func foo<erased E: Error>(fn: () throws E -> Void) rethrows Error

I think, it is equivalent to this:

func foo(fn: () throws Error -> Void) rethrows Error

which kind of is C. But important is, that this behavior must stay the same, if we don't want to break any sources.
This knowledge does IMHO just naturally lead to my solution, which doesn't break sources either.
Yes, with this solution we would need to change e.g. map to this:

func map<E, T>(_ transform: (Element) throws E -> T) rethrows E -> [T]

which is unfortunate, but I can't see any way around this.
Notice, that I don't propose, to add changes of e.g. map in this proposal, but it is good if we have the ability, to make them in the future.

1 Like

For the type inference issue I would extend solution 2 to the following:

throws functions should never be inferred to be the typed throw but the base throws, unless there is an explicitly specified typed throws. It would behave like follows:

struct SomeError: Error {}

func foo1(_ bar: () throws -> Int) {}

foo1 { throw SomeError() }
// bar has type `() throws -> Int`,
// there is no explicit type after `throws` in the declaration

foo1 { 5 }
// bar has type `() throws -> Int`,
// but we emit a warning, that no error is thrown


func foo2<T>(_ bar: T) {}

foo2 { throw SomeError() }
// bar has type `() throws -> ()`,
// we don't infer `SomeError` as the thrown type here,
// because again there is no explicit type after `throws`
// in the declaration (we consider not having an explicit
// `throws` or an explicit function type at all the same)


func foo3<E>(_ bar: () throws E -> Int) {}

foo3 { throw SomeError() }
// bar has type `() throws SomeType -> Int`,
// because there is an explicit typed throws

foo3 { 5 }
// bar has type `() throws Never -> Int` or `() -> Int` for short

Variables:

let value = { throw SomeError() }
// value has type `() throws -> ()`,
// again there was no type explicitly specified typed throws

// if we want a variable to have a typed throws, we declare it like so
let value1: () throws SomeError -> () = { throw SomeError() }


// the same applies to other types, like arrays, dictionaries, tuples, etc.
let array1 = [{ throws SomeError() }]
// array1 has type `[() throws -> ()]`

let dict1  = ["foo": { throws SomeError() }]
// dict1 has type `[String: () throws -> ()]`

let tuple1 = (5, { throws SomeError() })
// tuple1 has type `(Int, () throws -> ())`


// to make them have typed throws:
let array2: [() throws SomeError -> ()] = [{ throws SomeError() }]

let dict2: [String, () throws SomeError -> ()] = ["foo": { throws SomeError() }]

let tuple2: (Int, () throws SomeError -> ()) = (5, { throws SomeError() })

Other scenarios behave predictably:

struct Foo<T> {
    let t: T
}

let value2 = Foo { throw SomeError() }
// value2 has type `Foo<() throws -> ()>`,
// because `T` has no explicit typed throws

let value3: Foo<() throws -> ()> = Foo { throw SomeError() }
// or
let value4 = Foo<() throws -> ()>(t: { throw SomeError() })


struct Bar<E> {
    let bar: () throws E -> ()
}

let value5 = Bar { throw SomeError() }
// value5 has type `Foo<() throws SomeError -> ()>`,
// because bar has a function type with explicitly specified typed throws

This would be my solution to this problem. I think, it's a good trade-off between keeping source-stability and having expressivity.

1 Like

This is my favourite solution for

  • not breaking source compat
  • don't advancing the throws keyword (e.g. @typed throws)

But maybe the majority prefers advancing the keyword more. But I will point this out in the discussion document. Thx a lot.

We put together all the approaches we have until now here: https://github.com/minuscorp/swift-typed-throws/blob/master/Typed%20throws%20discussion.md#throw-and-type-inference

1 Like

We are now three people working on the implementation, but we all have little to no experience with the Swift compiler whatsoever. This gets tricky, especially when we try to implement type-checking (the swift type-checker is the single most scary piece of code I've ever seen). I spent the whole afternoon, only trying to implement type-checking of the type after throws.

Would someone with a bit more knowledge about the type-checker volunteer, to at least give us some tips? That would already really help. Just DM me ;)

I would recommend posting your implementation related questions on #development:compiler. We can continue talking about type-checking the throws type there.

Just for let you know, we're still in development, it is a pretty big proposal to implement :smile:

6 Likes

Let me show an option though I am not sure if it is good.

Currently, it is discussed that the following two are equivalent.

func foo() throws
func foo() throws Error

In the context of "Improving the UI of generics", it can be written as shown below.

func foo() throws any Error

Instead of it, I think plain throws can be interpreted as throws some Error.

func foo() throws some Error

throws some Error has an advantage of performance because it does not require existential containers when it is specialized.

A problem of throws some Error is cases of throwing multiple types of errors in one function.

// `throws` stands for `throws some Error`
func foo() throws {
    if Bool.random() {
        throw AError()
    } else {
        throw BError()
    }
}

On the analogy of opaque result types, it seems like to cause a compilation error. However, because Error has self-conformance, any Errors can be throws as some Errors. Thus the code above can be interpreted as the following one.

func foo() throws some Error {
    if Bool.random() {
        throw AError() as any Error
    } else {
        throw BError() as any Error
    }
}

Then it could be possible to introduce throws as throws some Error without any source-breaking changes.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy