[swift-evolution] [Proposal Idea] catching functions for composable and cps error handling
Matthew Johnson
matthew at anandabits.com
Thu Dec 17 10:36:25 CST 2015
Brent, thanks for sharing this. It’s interesting that we independently came up with such similar approaches. That seems to indicate that it is definitely worthy of consideration.
IMO your proposal is actually three proposals. I obviously think “catching functions” might have a chance for consideration right now. I also think @once is a very good idea and probably has a good chance for consideration as an independent proposal. Obviously async is going to have to wait.
I’m going to comment on differences between our approaches to catching functions and include the rationale for my decisions. I hope you will do the same and maybe we can reach a consensus on the best approach.
> A function type can be marked as `catching`.
>
> catching T -> U
I prefer the T -> U catches syntax as it more closely matches the syntax for throws, but the syntax is the least important part of this to me and I would be happy with anything sensible.
> Catching functions must be exhaustive (i.e. must include a plain `catch` block), with two exceptions to be described later.
Rather than requiring this I introduced an @exhaustive attribute that can be specified when required, as well as the ability of the caller to find out whether the error was handled or not. I did this partly because of the comparability use case and partly because it affords more flexibility without losing the exhaustive behavior when that is necessary.
> Catch blocks have the same return type as the regular block of their function. For instance, the catch blocks of a `catching Void -> Int` must return `Int`s. If the function is marked `throws`, then the catch blocks can `throw`.
>
> func convertOnlyCloudErrors() throws -> String {
> return “OK”
> }
> catch let error as CKErrorCode {
> return error.description
> }
> catch {
> throw error
> }
Did you intend to mark this function as `catching`? I’ll assume so as it includes top level `catch` clauses.
This approach is reasonable, but I’m interested in hearing what you think of the other alternative I explored which goes hand-in-hand with non-exhaustive catching functions.
> Here’s the first exception to catch exhaustiveness: a `catching throws` function has an implicit `catch { throw error }` block added if necessary. So that second catch block in the previous example is redundant.
I don’t like this. I think it is better to keep things explicit. It would also preclude non-exhaustive `catching` functions which I think have interesting use cases.
>
> To call the function normally, just…call it normally.
>
> foo()
> cloudKitQueryOp.queryCompletionBlock!(cursor)
>
> To send an error to it, use one of these:
>
> foo(throw error)
> foo(try someOperationThatMightFail()) // unconditionally calls foo() with the result, whether error or datum
> foo(try? someOperationThatMightFail()) // if an error is thrown, calls foo with the error and returns nil; if not, returns the result of the operation without calling foo
>
> I’m not totally satisfied with this syntax, and we should probably try to come up with something better. BUT NOT RIGHT NOW. Bikeshedding can wait.
I don’t mean to bikeshed but I do think this particular syntax has serious problems:
func bar() throws {
// in both cases, should the error be thrown or caught by foo?
foo(throw error)
foo(try someOperationThatMightFail())
}
It might be possible to define this problem away, but even then you would not know the answer without knowing the signature of `foo`. That is really bad for readability.
What do you think of the syntax I used, which is similar to yours while avoiding these issues?
foo(catch error)
> One issue with this is the question of whether a `throw` or non-optional `try` with a catching function implicitly `return`s immediately after calling the catching function. My current thinking is that it does *not* cause an immediate return, so you can come up with a return value yourself. But this is not entirely satisfying.
This is only a problem because your syntax overloaded the existing throw and try keywords and because of that introduced semantic confusion.
> To Objective-C, a catching function is a block with the same parameters, only nullable, followed by a nullable NSError. If there are no parameters, then an initial BOOL parameter is inserted.
>
> @property (nonatomic, copy, nullable) void (^queryCompletionBlock)(CKQueryCursor * __nullable cursor, NSError * __nullable operationError);
> var queryCompletionBlock: (catching CKQueryCursor -> Void)?
Glad to see you address Objective-C interop. I hadn’t considered that yet.
Matthew
More information about the swift-evolution
mailing list