Due to the large number of posts split between different threads I'm confused as to whether concrete error types will be allowed, e.g.
func a() throws(MyError) {}
do {
a()
} catch(error) {
// Is this allowed or will `error` be of type `any Error`?
error as MyError
}
And if concrete errors are allowed, what's the strategy to prevent the proliferation of precise error typing over the sensible default of type-erased errors? Also, will embedded platforms retain Standard Swift's type-erased errors and then aggressively inline, or will it exclusively use concrete error types to avoid dynamic casting?
For libraries that want to provide a consistent API across embedded and full-featured Swift targets, I wonder if throws(some Error) could be a way to minimize the API difference between targets. Outwardly that would give callers about the same amount of information they're allowed to code against as throws(any Error), allowing for API evolution to change the error type under the hood, though on embedded platforms functions would still be restricted to throwing a single concrete underlying type.
Doesnât Error being self-conforming make some Error fairly equivalent to any Error? In other words, why not make throws(some Error) the fully expanded spelling of âuntypedâ errors, and ban throws(any Error)?
Then I guess the next question is for @Douglas_Gregor and @John_McCall: does throws(some Error) solve all the embedded-specific needs without introducing the ergonomic pitfalls of errors with exposed types?
Even in "full" Swift I wouldn't jump to the conclusion that it's a drop-in replacement for untyped throws, or that supporting throws(some Error) is less work than supporting typed throws in the general case. The opaque type still represents some concrete type, tied to a specific declaration's underlying implementation, and as soon as you use it as a closure or through some other abstraction we would need to either propagate that opaque type or erase it to any Error, and you would get type inequalities between different declarations' thrown errors like you do with other opaque return types.
The struct should be inferred to be uninhabited when T: Uninhabited, but its layout tells us nothing about it. It's impossibility to construct a value of this type makes it uninhabited.
But inits aren't always visible. So automatic conformance to Uninhabited could be challenging.
It's certainly possible for enums and types with stored properties of uninhabited types. But in general case it seems a bit too much for an automated conformance.
Another example:
struct S {
init() { fatalError() }
}
Here we do have a callable init, and still this type is uninhabitable.
I guess there should be an option to manually conform to Uninhabited protocol (with where clause) that will require from a type to have no reachable ways to get a value.
Fair point; I oversimplified the example. Use X<() -> Void> and X<() throws(T) -> Void> where you don't have a sub typing relationship.
For something like fopen, which abstracts over a number of underlying file systems and is likely to grow more in the future, untyped throws is the better answer than trying (and failing) to list the potential failure types at some moment in time.
I don't expect to solve that issue with typed throws. It could help with some aspects of AsyncSequence, such as being able to capture the thrown error type in an associated type.
Yes, an opaque throws could be supported under the constraints of Embedded Swift. Opaque types in non-resilient libraries are hidden from the user, but not from the compiler. Note that opaque throws will have some annoying expressivity issues, i.e.,
I'm not going to dispute this statement, at least not in this thread. This is a matter of approach to API design, and not something important for the implementation of the feature. I think we should stop at the point that we agree that throws should support exactly one error.
@Douglas_Gregor Have you thought about whether there's an Obj-C story here as well? It would be great if Obj-C code could be annotated to provide the Swift error type the function should throw as part of the automatic mapping. I'm not sure how to handle an incorrect type here yet but at the least it's an opportunity for Obj-C to provide a type checked at runtime to ensure only expected errors are returned. Personally I'd like it if URLSession could realize they've let some POSIXErrors fall through the cracks before they bubble up.
Although that does then raise the question of what should happen if the Objective-C code violates the contract at runtime? Do you throw yet another exception (that's also the wrong type)? Seems pointless. Just crash? Seems severe, although well-precedented with e.g. how nil tends to get handled.
My point wasnât to argue that itâs simpler to implement. It was to highlight that the demand for âtyped throwsâ comes from people who want to be able to exhaustively enumerate all possible errors a function may emit, whereas @Douglas_Gregorâs motivation for starting this implementation was to implement throws in a runtime without existentials. The existence of opaque types means solving the latter does not require allowing the former.
It might make a lot of people in this thread mad, but if the motivation for hacking on this really is embedded Swift, then I believe the plan should be to only support some Error.
But then there's no way for callers to handle the error based on its actual type, as they'd only have available to them what's in the protocol (basically nothing). Normally this is accomplished with dynamic casting, which I assume would also be unavailable in embedded Swift. The other option would be to have some compile-time type specialization available to callers, but in my opinion this would be brittle and probably end up being typed errors with extra steps.
Iâm not sure this is actually such a fatal situation. Embedded programs are far more likely to rely on integer error codes than dynamic constructs such as exception hierarchies, for multiple reasons: one being the historical biases of C, but another being the extremely tight bounds on memory usage and algorithmic complexity guaranteed by such a simple scheme.
Besides, error introspection is not the only place where dynamic casts are currently used and would be unavailable in an embedded environment.
Instead of allowing the programmer to dynamically switch over error types, why not elevate the domain and code properties from NSError to the Error protocol, and vend a concrete type (GenericError, perhaps?) from the stdlib that library authors can use in embedded or full-fledged environments?
In practice, weâre talking about case .myErrorDomain rather than if let _ = error as? MyError. This doesnât introduce a prohibition on extended error information, but it also doesnât immediately encourage library authors to make the mistake of trying to fully specify their error domains in the type system.
Typed throws are just as an important carriers of static type information as things like classes. What you are saying is equivalent to saying "class instances should not be allowed to have a specific class type in resilient libraries and should always be AnyObject". This is obviously very wrong. The possibility of accidentally locking oneself into a specific error type and then regretting it after a major version release is not a language problem, it's an engineering incompetence problem. The author should just take the precaution to design their error type with future expansion in mind (e.g. a struct with optional metadata instead of a bare enum).
While we're on the topic:
Same goes for anonymous union types (A | B) which are nothing more than syntactic sugar around certain generic enums (e.g. Either<A, B>). This is nothing new and Swift is already perfectly capable of expressing this kind of type, so the "commonly rejected proposal due to compiler complexity" that I keep hearing is also obviously very wrong.
Conclusion:
The type system must be as expressive as the engineer sees fit to make their code expressive. Just because someone can't think of a reason to retain static type information (be that by using a specific error type or using an anonymous union type), doesn't mean there is no reason.