> On Feb 23, 2017, at 10:58 AM, Anton Zhilin <[email protected]> wrote: > > See some inline response below. > Also, have you seen the issue I posted in Proposal thread? There is a way to > create an instance of "any" type.
Yes, I saw that. There is no problem with that at all. As I point out in the analysis below, rethrowing functions are allowed to throw any error they want. They are only limited by *where* they may throw. > > 2017-02-23 3:37 GMT+03:00 Matthew Johnson via swift-evolution > <[email protected] <mailto:[email protected]>>: > # Analysis of the design of typed throws > > ## Problem > > There is a problem with how the proposal specifies `rethrows` for functions > that take more than one throwing function. The proposal says that the > rethrown type must be a common supertype of the type thrown by all of the > functions it accepts. This makes some intuitive sense because this is a > necessary bound if the rethrowing function lets errors propegate > automatically - the rethrown type must be a supertype of all of the > automatically propegated errors. > > This is not how `rethrows` actually works though. `rethrows` currently > allows throwing any error type you want, but only in a catch block that > covers a call to an argument that actually does throw and *does not* cover a > call to a throwing function that is not an argument. The generalization of > this to typed throws is that you can rethrow any type you want to, but only > in a catch block that meets this rule. > > > ## Example typed rethrow that should be valid and isn't with this proposal > > This is a good thing, because for many error types `E` and `F` the only > common supertype is `Error`. In a non-generic function it would be possible > to create a marker protocol and conform both types and specify that as a > common supertype. But in generic code this is not possible. The only common > supertype we know about is `Error`. The ability to catch the generic errors > and wrap them in a sum type is crucial. > > I'm going to try to use a somewhat realistic example of a generic function > that takes two throwing functions that needs to be valid (and is valid under > a direct generalization of the current rules applied by `rethrows`). > > enum TransformAndAccumulateError<E, F> { > case transformError(E) > case accumulateError(F) > } > > func transformAndAccumulate<E, F, T, U, V>( > _ values: [T], > _ seed: V, > _ transform: T -> throws(E) U, > _ accumulate: throws (V, U) -> V > ) rethrows(TransformAndAccumulateError<E, F>) -> V { > var accumulator = seed > try { > for value in values { > accumulator = try accumulate(accumulator, transform(value)) > } > } catch let e as E { > throw .transformError(e) > } catch let f as F { > throw .accumulateError(f) > } > return accumulator > } > > It doesn't matter to the caller that your error type is not a supertype of > `E` and `F`. All that matters is that the caller knows that you don't throw > an error if the arguments don't throw (not only if the arguments *could* > throw, but that one of the arguments actually *did* throw). This is what > rethrows specifies. The type that is thrown is unimportant and allowed to be > anything the rethrowing function (`transformAndAccumulate` in this case) > wishes. > > Yes, upcasting is only one way (besides others) to convert to a common error > type. That's what I had in mind, but I'll state it more explicitly. The important point is that if you include `rethrows` it should not place any restrictions on the type that it throws when its arguments throw. All it does is prevent the function from throwing unless there is a dynamic guarantee that one of the arguments did in fact throw (which of course means if none of them can throw then the rethrowing function cannot throw either). > > ## Eliminating rethrows > > We have discussed eliminating `rethrows` in favor of saying that non-throwing > functions have an implicit error type of `Never`. As you can see by the > rules above, if the arguments provided have an error type of `Never` the > catch blocks are unreachable so we know that the function does not throw. > Unfortunately a definition of nonthrowing functions as functions with an > error type of `Never` turns out to be too narrow. > > If you look at the previous example you will see that the only way to > propegate error type information in a generic function that rethrows errors > from two arguments with unconstrained error types is to catch the errors and > wrap them with an enum. Now imagine both arguments happen to be non-throwing > (i.e. they throw `Never`). When we wrap the two possible thrown values > `Never` we get a type of `TransformAndAccumulateError<Never, Never>`. This > type is uninhabitable, but is quite obviously not `Never`. > > In this proposal we need to specify what qualifies as a non-throwing > function. I think we should specifty this in the way that allows us to > eliminate `rethrows` from the language. In order to eliminate `rethrows` we > need to say that any function throwing an error type that is uninhabitable is > non-throwing. I suggest making this change in the proposal. > > If we specify that any function that throws an uninhabitable type is a > non-throwing function then we don't need rethrows. Functions declared > without `throws` still get the implicit error type of `Never` but other > uninhabitable error types are also considered non-throwing. This provides > the same guarantee as `rethrows` does today: if a function simply propegates > the errors of its arguments (implicitly or by manual wrapping) and all > arguments have `Never` as their error type the function is able to preserve > the uninhabitable nature of the wrapped errors and is therefore known to not > throw. > > Yes, any empty type should be allowed instead of just `Never`. That's a > general solution to the ploblem with `rethrows` and multiple throwing > parameters. It looks like you clipped out the section "Why this solution is better” which showed how `rethrows` is not capable of correctly typing a function as non-throwing if it dynamically handles all of the errors thrown by its arguments. What do you think of that? In my opinion, it makes a strong case for eliminating rethrows and introducing the uninhabited type solution from the beginning. > > ### Language support > > This appears to be a problem in search of a language solution. We need a way > to transform one error type into another error type when they do not have a > common supertype without cluttering our code and writing boilerplate > propegation functions. Ideally all we would need to do is declare the > appropriate converting initializers and everything would fall into place. > > One major motivating reason for making error conversion more ergonomic is > that we want to discourage users from simply propegating an error type thrown > by a dependency. We want to encourage careful consideration of the type that > is exposed whether that be `Error` or something more specific. If conversion > is cumbersome many people who want to use typed errors will resort to just > exposing the error type of the dependency. > > The problem of converting one type to another unrelated type (i.e. without a > supertype relationship) is a general one. It would be nice if the syntactic > solution was general such that it could be taken advantage of in other > contexts should we ever have other uses for implicit non-supertype > conversions. > > The most immediate solution that comes to mind is to have a special > initializer attribute `@implicit init(_ other: Other)`. A type would provide > one implicit initializer for each implicit conversion it supports. We also > allow enum cases to be declared `@implicit`. This makes the propegation in > the previous example as simple as adding the `@implicit ` attribute to the > cases of our enum: > > enum TransformAndAccumulateError<E, F> { > @implicit case transformError(E) > @implicit case accumulateError(F) > } > > It is important to note that these implicit conversions *would not* be in > effect throughout the program. They would only be used in very specific > semantic contexts, the first of which would be error propegation. > > An error propegation mechanism like this is additive to the original proposal > so it could be introduced later. However, if we believe that simply passing > on the error type of a dependency is often an anti-pattern and it should be > discouraged, it is a good idea to strongly consider introducing this feature > along with the intial proposal. > > Will add to Future work section.
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
