Typed throws

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:

8 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

Just wanted to say thank you very much, @minuscorp, for pushing for this idea.

I've been out of the forum and the implementation due to several health issues, but this is not an idea I want to abandon at all. Once I'm recovered I'll try to start again and get this forward. Thank you for your comprehension and support, @eneko and the rest of the supporters.

1 Like

I wish you a speedy recovery. It might interest you that typed throws came up in another thread related to rethrows and protocol conformances. There's a design challenge buried in there from me ;)

Doug

In the first post there are documents that redirect to the pitch in the thread as well as some workarounds for some edge cases that came up, just in case you didn't notice them.
Anyway I'll read throughly the thread you mention and see if something we talked about in here can help you out. Also we left a pending implementation that might be useful in the future for the first step, that was make typed throws work into the system. Later iterations would solve subsequential issues until a final solution is ready to be proposed, that was our point of view about this whole topic if this post helps in something to you.

I’m a bit late here but

At most one type of specific error can be used with a throws .

Is this rule set in stone?

I understand that “sum types” are out of scope but, in my opinion, listing types is not a “sum type” but just, well, a list of types.

I would very much like to have a list of types thrown.

I think it would simplify many use cases, e.g. the family example would not have to be:

enum FamilyError : Error {
   case kid(_ e: KidError)
   case spouse(_ e: SpouseError)
   case cat(_ e: CatError)
}

func callFamily() throws FamilyError -> Family {
    do {
        let kids = try callKids()
        let spouse = try callSpouse()
        let cat = try callCat()
        return Family(kids: kids, spouse: spouse, cat: cat)
    } catch let error as CatError  {
        throw FamilyError.cat(error)
    } catch let error as SpouseError  {
        throw FamilyError.spouse(error)
    } catch let error as KidError{
        throw FamilyError.kid(error)
    }
}

but could be much simpler:

func callFamily() throws CatError, SpouseError, KidError -> Family {
    let kids = try callKids()
    let spouse = try callSpouse()
    let cat = try callCat()
    return Family(kids: kids, spouse: spouse, cat: cat)
}

I understand this just moves the catches to the call side. On the other hand, switching over FamilyError is the same amount of code.

For Result and rethrows, the compiler could use the “natural sum type”: Search the inheritance tree until a type is found that all thrown types conform to (the search is guaranteed to end at Error):

protocol ColoredError: Error { }
class BlueError: ColoredError { }
class DeepBlueError: BlueError { }
class RedError: ColoredError { }

func f() throws BlueError, DeepBlueError, RedError {}

is equal to

func f() -> Result<Void, ColoredError> {}

How are you supposed to catch this type of typed error? The point of of throwing just one type is the same rationale that a variable can only have one concrete type, to achieve what you propose I think you'll have to cast all types individually to check which one is, and if they are enums then cast which case is. Two levels of casting seems to complex to me. I don't know about other opinions though!

Imho it would already help if that annotation wouldn't change the behavior of the compiler at all (except maybe checking wether the error-types actually exist):
Documenting which errors users of your code can expect is useful on its own, even if there are no guarantees associated with that information.

It can be a thing to expand in a future work or a second iteration, first of all to let the user get used to the new syntax (unique thrown type) but throwing for example, a list of unrelated types, gives almost as little information in the client error treatment as if it were an untyped thrown error imo

As you do today:

} catch (let e as CatError) {
} catch (let e as SpouseError) {
} catch (let e as KidError) {

or, for the other example, you might as well use:

} catch (let e as RedError) {
// First match, most specific.
} catch (let e as ColoredError) {
// Catch everything else
}

I think it would be fine if you had to catch the “natural sum type” unless you catch all types declared in the throws-list.

With this approach we do not was ease the client part of a typed throws handling, it seems to me like just the same approach as we have today, but somehow those are typed when in practice they are the same as if they weren't.

Also, in the proposal all the subclassing topics are recorded and documented, maybe we missed something, but I think we do not need several typed errors, as it makes the handling and the writing linearly complex, as if you use subclassing with a single typed error that problem is gone.

As of Swift 5.3 thanks to SE-0276 we can now also handle typed errors as

} catch ColorError.red, ColorError.blue {
  // handle these cases
}

They are untyped though (the interface)

That should be corrected in the draft then, as you could handle same type errors in the same catch clause.

Terms of Service

Privacy Policy

Cookie Policy