Distinguishing single-call closures vs multi-call closures

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.

5 Likes

I doubt the compiler can prove that a closure is called once and only once in all cases.

This will put a significant burden on api authors who want to use this. All of Apple's APIs that already support a once semantic will have to be decorated with this attribute in order that new code can declare a once closure that is passed to those existing APIs. I don't know if that breaks backwards compatibility.

It's okay. The same problem of compiler not being smart enough to prove it in all cases exists for @escaping and we have a solution for that already! :) If something is too complicated to prove at compile time, you can use withoutActuallyEscaping and it will check the escapiness at runtime instead. I can imagine a similar function that checks onceness. It would also be a solution for using old nondecorated functions.

It doesn't break backwards compatibility, because today there are zero functions decorated with @once, so none of them will break. Adding @once to a public function in a library wouldn't break anything either, because it would allow more code to compile, not less.


(I have neutral opinion on the pitch itself)

1 Like

As a starting point, you could create a wrapper around the closure and check that it is called only once. You can use callAsFunction to make it feel more like a closure.

When Swift has movable types, you could declare your callAsFunction as consuming the value of the wrapper - this will make sure that closure is not called more than once.

And we are still left with ensuring that closure is called at least once.

Note that for async closures you need to tell compiler somehow what is the scope of the code that needs to call the closure at least once.

Some sort of “don’t quit from this function until closure is called”. And compiler already has a check that function with non-void result must return a value of that type on each execution path.

Let’s try to piggyback on this. You can introduce some type as an “call witness” - some value that can be produced only as the result of the closure, but is required to be returned from the code that must call the closure.

Note that this does not need to be a concrete type. You can make all the code that must call the closure as generic on the closure return type. If that type does not have any protocol witnesses, the only way to get an instance of that type - is by calling a closure.

And existing optimizations should be able to replace generic code with the one specialized for Void

@Nickolas_Pohilets are you describing a proof-of-concept "using existing language constructs" solution or describing a final "update the compiler" solution?

I think @cukr described succinctly that how this would be possible by following the same logic in @escaping. Not sure if I'm really adding anything, but just going in more detail...

In the case of non-escaping closures:

func doSomething(completion: @once (Result<Success,Error>) -> Void) {
    ...
}

I think the compiler can just do the check at compile-time and provide warnings

However, for escaping closures:

func doAsyncSomething(completion: @once @escaping (Result<Success,Error>) -> Void) {
    ...
}

There is nothing the compiler can do at compile time. I think the easiest thing to do is to check the flag on call and on deinit and assert appropriately.

I think that implementation is straightforward to implement (probably? -- I haven't looked at the swift compiler source) and it's also straight-forward to understand.

@Nickolas_Pohilets are you describing a proof-of-concept "using existing language constructs" solution or describing a final "update the compiler" solution?

I’m mentioning move-only types which are not implemented yet, so probably the later.

The point that I’m trying to make is that use case of call-once closures can be modeled using move-only types + existing features.

Move-only types are already on the Swift roadmap, so assuming they will be implemented, we don’t need a separate language feature for call-once functions.

Compiler can check that function was called at least once even for escaping closures, but we need to describe the boundary for that somehow.

func doAsyncSomething<T>(completion: @escaping (Result<Success,Error>) -> T) -> Future<T> {
    ...
}

// De-facto T is always Void and should be optimized away
doAsyncSomething { result in ... }

In this case Future<T> gives us the boundaries of the async execution flow where completion must be called.

To resolve the returned future we need an instance of T.

But inside doAsyncSomething we know nothing about type T except that this a return type of the completion. So the only way to get an instance of T is by calling completion.

So we can be sure that if implementation of the doAsyncSomething type-checks, then all the code paths leading to resolution of the promise do call completion at least once.

And move-only types can be used to make sure that it is not called more than once.