[Proposal Idea] catching functions for composable and cps error handling


(Matthew Johnson) #1

I came up with an interesting idea that might make a nice enhancement to Swift’s current error handling. I’m curious to see whether others think it is worth pursuing or not.

There are two motivating examples for this. It could provide the obvious way to flow errors through code written in continuation passing style (whether asynchronous or not). It could also provide a way to abstract over common error handling code.

The basic idea is to allow catching functions that would be a complement to throwing functions. Catching functions would have one or more catch blocks at the top level and could also accept arguments:

func handler(int i: Int, str: String) catches {
  // i and str are in scope
} catch VendingMachineError.InvalidSelection {
  // i and str are not in scope
} catch VendingMachineError.OutOfStock {
  // i and str are not in scope
}

The function could be called as normal:
handler(int: 1, str: “”)

In this case the body would be executed.

The function could also be called with a “catch” clause:
handler(catch VendingMachineError.InvalidSelection)

In this case the top level catch clauses would be evaluated as if they part of a do-catch statement in which an error was thrown.

Note that there is no colon after the `catch` in the function call. This would avoid conflicting with a potential argument named catch and would also call attention to the fact that it is not a normal argument. I don’t think `throw` would be appropriate here as that could be ambiguous if the function containing the call to `handler` was a throwing function and also because an error would not be thrown up the stack, but rather caught directly by `handler`.

It may be worthwhile to consider requiring a catching function to handle all errors if it wishes to return a value so that it is able to return a value when catching an error no matter what the error is.

Alternatively, (and maybe more interesting) since the compiler knows at the call site whether the function was provided regular arguments or an error to catch these two cases could be handled independently, with the body returning a value and the result of a “catching” call returning a value indicating whether the error was handled or not (or something similar).

Here is how this would look when it is applied to the motivating examples.

First is a cps example using a catching closure.

// @exhaustive requires the catching function to catch all errors
func cps(then: Int -> () @exhaustive catches) {
  if (checkSomeState) {
    then(42)
  } else {
    then(catch VendingMachineError.InvalidSelection)
  }
}

cps() { i: Int in
  // do some work using i
} catch VendingMachineError.InvalidSelection {
  // handle the error, i is not in scope
} catch VendingMachineError.OutOfStock {
  // handle the error, i is not in scope
} catch {
  // handle all other errors
}

Second is an example showing how this could be used to abstract over error handling logic:

func handler() catches {
catch VendingMachineError.InvalidSelection {
  // some common code handling InvalidSelection
catch VendingMachineError.OutOfStock {
  // some common code handling OutOfStock
// handle some other cases as well
}

func doSomething() {

  do {

    try someThrowingFunction()

  } catch handler { // compiler inserts call: handler(catch errorThatWasThrown)

    // not sure if a body would make sense here
    // if it does it would only be executed when handler actually handled the error

  // we only proceed to the next case if handler did not handle the error
  } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {

  // we can provide arguments to the error handling logic by calling a function that returns a catching closure
  } catch someFunctionReturningAClosureThatCatches(arg: someValueDeterminingHowToHandleErrors)
  }

}


(Brent Royal-Gordon) #2

The basic idea is to allow catching functions that would be a complement to throwing functions.

I have an enormous proposal on the subject of formalizing completion handlers which includes catching functions. I haven’t posted it because I figured it’d be out of scope for Swift 3, but I can if there’s interest.

···

--
Brent Royal-Gordon
Architechies


(Thorsten Seitz) #3

What about just writing the following (see below)? To enable the same behaviour we would have to add non-local return which would be nice for other reasons.

func handler<T>(block: () -> T) rethrows {
        do {
                let result = try block()
                return result
        } catch VendingMachineError .InvalidSelection {
                // some common code handling InvalidSelection
        } catch VendingMachineError.OutOfStock {
               // some common code handling OutOfStock
               // handle some other cases as well
        }
}

func doSomething() {
        do {
                handler {
                        try someThrowingFunction()
                }
        } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
                // special case handled here
        }
}

For covering your third case using a handler function with an argument, you could add an argument to the func handler.
For chaining handler functions you could introduce an operator for composing handler functions.

-Thorsten

···

Am 17.12.2015 um 06:27 schrieb Matthew Johnson via swift-evolution <swift-evolution@swift.org>:

I came up with an interesting idea that might make a nice enhancement to Swift’s current error handling. I’m curious to see whether others think it is worth pursuing or not.

There are two motivating examples for this. It could provide the obvious way to flow errors through code written in continuation passing style (whether asynchronous or not). It could also provide a way to abstract over common error handling code.

The basic idea is to allow catching functions that would be a complement to throwing functions. Catching functions would have one or more catch blocks at the top level and could also accept arguments:

func handler(int i: Int, str: String) catches {
  // i and str are in scope
} catch VendingMachineError.InvalidSelection {
  // i and str are not in scope
} catch VendingMachineError.OutOfStock {
  // i and str are not in scope
}

The function could be called as normal:
handler(int: 1, str: “”)

In this case the body would be executed.

The function could also be called with a “catch” clause:
handler(catch VendingMachineError.InvalidSelection)

In this case the top level catch clauses would be evaluated as if they part of a do-catch statement in which an error was thrown.

Note that there is no colon after the `catch` in the function call. This would avoid conflicting with a potential argument named catch and would also call attention to the fact that it is not a normal argument. I don’t think `throw` would be appropriate here as that could be ambiguous if the function containing the call to `handler` was a throwing function and also because an error would not be thrown up the stack, but rather caught directly by `handler`.

It may be worthwhile to consider requiring a catching function to handle all errors if it wishes to return a value so that it is able to return a value when catching an error no matter what the error is.

Alternatively, (and maybe more interesting) since the compiler knows at the call site whether the function was provided regular arguments or an error to catch these two cases could be handled independently, with the body returning a value and the result of a “catching” call returning a value indicating whether the error was handled or not (or something similar).

Here is how this would look when it is applied to the motivating examples.

First is a cps example using a catching closure.

// @exhaustive requires the catching function to catch all errors
func cps(then: Int -> () @exhaustive catches) {
  if (checkSomeState) {
    then(42)
  } else {
    then(catch VendingMachineError.InvalidSelection)
  }
}

cps() { i: Int in
  // do some work using i
} catch VendingMachineError.InvalidSelection {
  // handle the error, i is not in scope
} catch VendingMachineError.OutOfStock {
  // handle the error, i is not in scope
} catch {
  // handle all other errors
}

Second is an example showing how this could be used to abstract over error handling logic:

func handler() catches {
catch VendingMachineError .InvalidSelection {
  // some common code handling InvalidSelection
catch VendingMachineError.OutOfStock {
  // some common code handling OutOfStock
// handle some other cases as well
}

func doSomething() {

  do {

    try someThrowingFunction()

  } catch handler { // compiler inserts call: handler(catch errorThatWasThrown)

    // not sure if a body would make sense here
    // if it does it would only be executed when handler actually handled the error

  // we only proceed to the next case if handler did not handle the error
  } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {

  // we can provide arguments to the error handling logic by calling a function that returns a catching closure
  } catch someFunctionReturningAClosureThatCatches(arg: someValueDeterminingHowToHandleErrors)
  }

}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #4

I am definitely interested in seeing your proposal if you’ve already written it.

Async / concurrency, etc is explicitly out of scope. I think features like catching functions which have applications in asynchronous code but do not specifically address concurrency, synchronization, etc are at least worth discussing unless / until the core team specifically rules them out. My thought is that catching functions could be a natural evolution of the error handling introduced in Swift 2.

···

On Dec 16, 2015, at 11:42 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

The basic idea is to allow catching functions that would be a complement to throwing functions.

I have an enormous proposal on the subject of formalizing completion handlers which includes catching functions. I haven’t posted it because I figured it’d be out of scope for Swift 3, but I can if there’s interest.

--
Brent Royal-Gordon
Architechies


(Brent Royal-Gordon) #5

I am definitely interested in seeing your proposal if you’ve already written it.

[Proposal follows.]

Swift 2 codified error parameters, turning them into a language feature with dedicated syntax. I think most people are pretty happy with that. Everyone wants to do the same for async APIs, replacing the pyramids of nested completion handlers with something more sensible. But this is a difficult problem and will take time to figure out.

In the mean time, though, I think we can improve tremendously on completion handlers by codifying them and giving them dedicated syntax. And it won’t really change our eventual pyramid-of-doom replacement. There are hundreds of existing APIs which use completion handlers; whatever we come up with in the future will have to be compatible with them, and therefore, with this proposal.

My proposal comes in three parts.

CATCHING FUNCTIONS

···

----------------------------------

A function type can be marked as `catching`.

  catching T -> U

A catching function or closure has one or more `catch` blocks appended to it.

  func foo() {
    // Normal, non-error behavior
  }
  catch {
    // Handle errors thrown to this function
    // Possible point of confusion: errors thrown *in* the main body of foo() do *not* go here.
  }

  cloudKitQueryOp.queryCompletionBlock = { cursor in
    // non-error behavior
  }
  catch CKErrorCode.NotAuthenticated {
    nagUserToEnableCloud()
  }
  catch CKErrorCode.RequestRateLimited {
    retryLater()
  }
  catch {
    displayCloudError(error)
  }

Catching functions must be exhaustive (i.e. must include a plain `catch` block), with two exceptions to be described later.

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
  }

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.

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.

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.

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)?

ONCE ATTRIBUTE
--------------------------

This one is not strictly necessary, but it's very helpful for safety.

The `@once` attribute on a function-typed parameter indicates that the parameter will be called exactly once. Not zero times, not two times. Thrice is right out. Throwing to a catching function counts as “calling” it.

  func tooMany(@once fn: Void -> Void) {
    fn()
    fn()
  }
  func tooFew(@once fn: Void -> Void) {
  }
  func justRight(@once fn: Void -> Void) {
    fn()
  }

@once makes no guarantees about *when* the parameter will be called; it does not imply @noescape. It may be called asynchronously, after the function it’s passed to has finished executing. But it will still be called exactly once.

To the extent possible, swiftc should try to validate that the parameter really is called exactly once. Obviously, this won’t be possible if the parameter is called in a closure, unless that closure is passed to a parameter which is itself marked @once.

Note: It may be desirable to apply @once to function-typed properties as well—NSOperation and CloudKit’s NSOperation subclasses could certainly use it.

ASYNC FUNCTIONS
-----------------------------

A function may be marked `async`. This keyword goes after the argument list, but before `throws` or the return type arrow. An `async` function is simply one that takes a completion handler. So these two are equivalent:

  func fetch() async
  func fetch(@once completion: Void -> Void)

You can probably say `async(bar)` to rename the completion argument to `bar`, and `async(bar baz)` to name the variable `baz` and the keyword `bar`. Whatever—it’s not very important.

The `async` keyword is magical in that it hijacks the function’s return type, turning it into an argument to the completion. So these two are equivalent:

  func fetch() async -> MyRecord
  func fetch(@once completion: MyRecord -> Void)

It also hijacks the `throws` specifier, turning it into a `catching` specifier on the completion. So these two are equivalent:

  func delete() async throws
  func delete(@once completion: catching Void -> Void)

As are these:

  func fetch() async throws -> MyRecord
  func fetch(@once completion: catching MyRecord -> Void)

Inside the body of an `async throws` function, all errors thrown with `try` or `throw`, and otherwise not caught by anything, are implicitly directed to `completion`.

  func fetch() async throws -> MyRecord {
    guard let recordURL = recordURL else {
      throw MyRecordError.NoRecordURL // this is really `completion(throw MyRecordError.NoRecordURL); return`
    }
    
    fetchURL(recordURL) { data in
      let record = try MyRecord(data: data) // this is really something like `let record = completion(try? MyRecord(data: data)) ?? return`
      completion(record)
    }
    catch {
      throw error // this is also really `completion(throw error); return`
    }
  }

Note that this occurs even in the main body of the async function. That means the completion may be called before the async function returns.

Remember how I said there was a second exception to the rule that `catching` functions must have an exhaustive set of `catch` clauses? That exception is inside an `async throws` function. There, they automatically have a `catch { completion(throw error) }` clause added. To indicate this hidden `throw`, however, you have to add `try` to the statement that creates the closure. So the above could have been written:

  func fetch() async throws -> MyRecord {
    try fetchURL(recordURL) { data in
      let record = try MyRecord(data: data)
      completion(record)
    }
    // Look, Ma, no catch!
  }

At the site where an `async` function is called, absolutely nothing changes. It is exactly equivalent to the completion-block form.

Objective-C APIs with a trailing block parameter labeled `completion` or `completionHandler` should have `@once` implicitly added to this parameter, allowing them to be treated as `async` methods.

CONCLUSION
--------------------

This proposal radically improves the experience of creating an asynchronous call by formalizing the notion of a function which takes a result-or-error, hugely reducing the amount of boilerplate involved in defining an async function, and introducing new safety checks to help you ensure you return data properly from an async function. It even improves the call site modestly by better organizing error-handling code. And it doesn’t interfere with any future work on new ways of representing calls to asynchronous functions.

--
Brent Royal-Gordon
Architechies


(Matthew Johnson) #6

Yes of course you can do these things. And you could use a Result type for error handling before Swift 2. The problem is that these are ad-hoc approaches and everyone will do it slightly differently.

Error handling is important enough to justify a language feature that aids correctness and establishes a common practice. This idea may or not be the right solution. Either way I do believe Swift will be better off with a language level solution.

···

On Dec 17, 2015, at 12:13 AM, Thorsten Seitz <tseitz42@icloud.com> wrote:

What about just writing the following (see below)? To enable the same behaviour we would have to add non-local return which would be nice for other reasons.

func handler<T>(block: () -> T) rethrows {
        do {
                let result = try block()
                return result
        } catch VendingMachineError .InvalidSelection {
                // some common code handling InvalidSelection
        } catch VendingMachineError.OutOfStock {
               // some common code handling OutOfStock
               // handle some other cases as well
        }
}

func doSomething() {
        do {
                handler {
                        try someThrowingFunction()
                }
        } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
                // special case handled here
        }
}

For covering your third case using a handler function with an argument, you could add an argument to the func handler.
For chaining handler functions you could introduce an operator for composing handler functions.

-Thorsten

Am 17.12.2015 um 06:27 schrieb Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>>:

I came up with an interesting idea that might make a nice enhancement to Swift’s current error handling. I’m curious to see whether others think it is worth pursuing or not.

There are two motivating examples for this. It could provide the obvious way to flow errors through code written in continuation passing style (whether asynchronous or not). It could also provide a way to abstract over common error handling code.

The basic idea is to allow catching functions that would be a complement to throwing functions. Catching functions would have one or more catch blocks at the top level and could also accept arguments:

func handler(int i: Int, str: String) catches {
  // i and str are in scope
} catch VendingMachineError.InvalidSelection {
  // i and str are not in scope
} catch VendingMachineError.OutOfStock {
  // i and str are not in scope
}

The function could be called as normal:
handler(int: 1, str: “”)

In this case the body would be executed.

The function could also be called with a “catch” clause:
handler(catch VendingMachineError.InvalidSelection)

In this case the top level catch clauses would be evaluated as if they part of a do-catch statement in which an error was thrown.

Note that there is no colon after the `catch` in the function call. This would avoid conflicting with a potential argument named catch and would also call attention to the fact that it is not a normal argument. I don’t think `throw` would be appropriate here as that could be ambiguous if the function containing the call to `handler` was a throwing function and also because an error would not be thrown up the stack, but rather caught directly by `handler`.

It may be worthwhile to consider requiring a catching function to handle all errors if it wishes to return a value so that it is able to return a value when catching an error no matter what the error is.

Alternatively, (and maybe more interesting) since the compiler knows at the call site whether the function was provided regular arguments or an error to catch these two cases could be handled independently, with the body returning a value and the result of a “catching” call returning a value indicating whether the error was handled or not (or something similar).

Here is how this would look when it is applied to the motivating examples.

First is a cps example using a catching closure.

// @exhaustive requires the catching function to catch all errors
func cps(then: Int -> () @exhaustive catches) {
  if (checkSomeState) {
    then(42)
  } else {
    then(catch VendingMachineError.InvalidSelection)
  }
}

cps() { i: Int in
  // do some work using i
} catch VendingMachineError.InvalidSelection {
  // handle the error, i is not in scope
} catch VendingMachineError.OutOfStock {
  // handle the error, i is not in scope
} catch {
  // handle all other errors
}

Second is an example showing how this could be used to abstract over error handling logic:

func handler() catches {
catch VendingMachineError .InvalidSelection {
  // some common code handling InvalidSelection
catch VendingMachineError.OutOfStock {
  // some common code handling OutOfStock
// handle some other cases as well
}

func doSomething() {

  do {

    try someThrowingFunction()

  } catch handler { // compiler inserts call: handler(catch errorThatWasThrown)

    // not sure if a body would make sense here
    // if it does it would only be executed when handler actually handled the error

  // we only proceed to the next case if handler did not handle the error
  } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {

  // we can provide arguments to the error handling logic by calling a function that returns a catching closure
  } catch someFunctionReturningAClosureThatCatches(arg: someValueDeterminingHowToHandleErrors)
  }

}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #7

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


(Matthew Johnson) #8

I’m bumping this post in case you missed it Brent. I would find any comments you have on the differences between our approaches very interesting. Thanks!

···

On Dec 17, 2015, at 10:36 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

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
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution