Hi all,
I've noticed that closures can be ambiguous in the number of times they are called. In most use-cases, what I care to know is whether this is a case where the closure will occur:
- Exactly once
- Zero or more times
Completion handlers are a common convention and it is generally understood that they will be called once. However the lack of a definitive declaration for this intent leads to confusion and errors.
For example:
class Network {
func callServer(completion: @escaping (Data?, Error?) -> Void) {
... underlying network implementation ...
guard networkStatus.IsValid() else {
completion(nil, Failure.InvalidStatus)
}
guard let data = data else {
completion(nil, Failure.NoData)
}
... more failure checking conditions: de-serialization, data validation, etc.) ...
completion(data)
}
}
There's a couple points of ambiguity and potential errors:
- As the function author, I must be very careful to ensure that the completion handler is called and called only once. But with complicated error handling or business logic, it can be easy to make mistakes resulting in the completion handler being called multiple times or not called at all.
- As a user to the function, I cannot be sure that the completion handler will ever be called. And in cases where the closure is not named "completion", I have little context as to whether it's possible for this closure to be called once, multiple times, or not at all.
I think this may be easily solved by introducing another tag, perhaps "@once"? (I know Pitch '@once closure' exists, but it appears to be talking about something else if I understand it correctly. And SE-0073 exists, but the arguments against seem to be mostly about the @noescape tag and syntax?)
My intent for this @once tag is to declare that the closure must be executed once-and-only-once before release, and hopefully only as a compile-time check.
By declaring a closure as @once in this way, we are ensuring that non-escaping and escaping closures must:
- be called before the end of scope, or
- passed to a function also declaring the closure as @once before the end of scope
It would be considered a compile-time error if both were to occur.
In addition, @escaping closures may store the closure in a variable also declared as @once before leaving scope. It would of course be an error to do this AND any of the prior steps.
Ideally, the compiler should ensure that the stored closure variable is executed before it is released. However, I'm not sure if that's possible at compile-time? Perhaps the best it can do is ensure that the stored closure will be passed to a function eventually? Of course a run-time "didExecute" flag that is checked when the closure variable is released may work and would still provide some developer benefit, but I'm not sure of the impact of this.
Assuming there is a way to do that, by introducing @once in this way, there should be no backward compatibility issues.