It was a illustrative behavior that cannot be achieved due to catch exhaustiveness, fatalError
doesn't seems to me as a good workaround.
But you're changing the code from non typed to typed, so you're responsible of that source breaking doesn't it?
Sorry, I edited while you were replying. Should be clearer now that foo
does compile pre-proposal, but would fail to compile post-proposal.
We'll consider this change, we don't want to break source compatibility. So rethrows
should not inherit any typed error from its inner throwing closures.
In this case, I'd really like to simply go with:
func callAndFeedCat2() -> Result<Cat, CatError> {
Result { try callCatOrThrow() }
}
Currently, I have to mapError
as below, and it'll crash rather than compile error when the error type is changed in the future.
func callAndFeedCat() -> Result<Cat, CatError> {
Result { try callCatOrThrow() }
.mapError { $0 as! CatError })
}
You cannot do that if callCatOrThrow()
is not throws CatError
Hey thanks to all of you for feedback
I want to point you all to the section Source compatibility, because that's where I see the main risk for the proposal, where we need a solution first. After that we can go into more detail with the other topics.
Thx
There probably isn't much leeway to rethrow
. Wrapping the error thrown by one of the function's argument is arguably practical. We'd need another ceremony for rethrows
to use the error type, at which point the merit hardly outweigh the added complexity.
So the most straightforward solution here is to make rethrows to always erase the error type.
Which would feel almost like a deal breaker to me. If I can't map
in a do block without erasing the error, the gain get's smaller and smaller?
Could you explain why this restriction is in place? For untyped errors, you need some information, hence Error
(there's also other implementation reasons...). For a typed throw, you already know the type, so why force the type to conform to Error
?
What is a "needless" catch
clause?
I don't understand why there is a source compat concern here? We do not have typed throws in the language today, so it is not possible that someone is calling a function which has a typed throw. There are similar points about source compat in Scenario 5.
Are you suggesting a change to the standard library here? Can this change be done in a binary compatible way? I suspect at least the name mangling might need to change, if not the calling convention as well...
I suspect this is not going to be ergonomic without an additional level of implicit wrapping. If I want to combine cases:
enum DownloadError {
case parse(ParseError)
case filesystem(FilesystemError)
case network(NetworkError)
}
func downloadFile() throws IOError {
do {
let configBlob = try readConfigFile()
let url = try configBlob.parseUrl()
try url.download()
} catch error as ParseError { throw .parse(error) }
} catch error as FilesystemError { throw .filesystem(error) }
} catch error as NetworkError { throw .network(error) }
}
So it only works well up to "one layer" and then you need to pick verbosity or you pick untyped errors.
For one, it allows () throws T -> Void
to always be a subtype of () throws -> Void
.
When you have already caught all typed errors (and there are no untyped error) the code will never be executed.
It is more an additive change than a substitute. But we already talked about that, it seems that it is not needed that overload
Sorry, but why wouldn't this compile? The compiler can see that the only possible error is a CatError, and besides, the second clause is a catchall, so why would there be an issue of "not being able to check exhaustiveness"?
That snippet wonāt compile because in the last catch block, the type of error
is Error
but the Resultās Failure
type is CatError
. The last catch block is needed because the catch isnāt exhaustive if you just handle CatError
, since callCatOrThrow()
can throw any Error
, so catch needs to handle either Error
or any subtypes of Error
and Error
itself.
It's not self-evident that this is a good thing... Why not () throws T -> Void
let be a subtype of () throws -> Void
only when T
extends Error
? Strictly more code will compile with that alternative.
do {
throw Foo()
} catch {
// error is Error but if we use inference from the single throw error is Foo.
}
So there you have the source compatibility issue why the magic catch that infers the type being thrown from the do clause.
IMHO this should get more attention in this proposal, because it solves issues with rethrows
and has very likely other use cases as well.
I currently had a problem with rethrowing, where I basically wanted to make a function rethrows
, but not based on a parameter. Something like that:
struct Foo {
let maybeThrowing: () throws -> ()
init(_ maybeThrowing: () throws -> ()) {
self.maybeThrowing = maybeThrowing
}
func bar() rethrows {
try maybeThrowing()
}
}
let foo1 = Foo { print("Foo") }
foo1.bar()
let foo2 = Foo { throw Error() }
do {
try foo2.bar()
} catch {
print("Error")
}
This obviously does not compile, but if every function implicitly threw Never
, we could rewrite it like that (Credits to this answer) :
struct Foo<E: Error> {
let maybeThrowing: () throws E -> ()
init(_ maybeThrowing: () throws E -> ()) {
self.maybeThrowing = maybeThrowing
}
func bar() throws E {
try maybeThrowing()
}
}
var foo1 = Foo { print("Foo") } // Foo<Never>
foo1.bar()
let foo2 = Foo { throw Error() } // Foo<Error>
do {
try foo2.bar()
} catch {
print("Error")
}
foo1 = foo2 // error: Cannot assign value of Foo<Error> to variable of type Foo<Never>
Another feature that we would basically get for free, if we made every function throw Never
, is this one:
This seems like a non-goal to meāat least as important as accepting as much code as possible, IMO, is ensuring that accepted code is easy to reason about and use. If typed throws
can throw non-Error
-conforming types, what is a user supposed to do with this:
func foo() throws Int { ... }
func bar() throws String { ... }
func doSomeStuff() /* ??? */ {
try foo()
try bar()
}