swiftsprite-kitstructured-concurrency

How can I indicate I want the synchronous version of a function when in an async context in Swift?


If you are using structured concurrency in Swift, and are in an async context, the compiler will assume that if an async version of a function exists, that is the one you want to use. Is there a way to tell it you want the synchronous version?

Here is an example (and to show that it is at least possible in theory).

In vanilla SpriteKit, if you have an SKNode and these lines:

node.run(SKAction.rotate(byAngle: -1.5*CGFloat.pi, duration: 6))
node.run(SKAction.scale(by: 25, duration: 6))

The node will rotate AND scale over the course of 6 seconds at the same time.

...if you are using structured concurrency run will use the async version of this function and the required code:

await node.run(SKAction.rotate(byAngle: -1.5*CGFloat.pi, duration: 6))
await node.run(SKAction.scale(by: 25, duration: 6))

Will cause the node to rotate over 6 seconds and THEN scale for 6 seconds. The completion callback is replaced with an async style flow control.

The problem is I don't want it to wait and return, but I don't see any way to say "Hey - just use the sync version of this function". If I just omit await it is a compiler error.

To show it is possible, I can force the use of a sync version of the function like this:

node.run(SKAction.rotate(byAngle: -1.5*CGFloat.pi, duration: 6)){}
node.run(SKAction.scale(by: 25, duration: 6)){}

...providing the completion trailing closure. So there isn't anything 'special' about the sync version of run. This works, but is there no other way to say "just use the sync function"?

I can also do this:

Task{ await node.run(SKAction.rotate(byAngle: -1.5*CGFloat.pi, duration: 6))}
Task{ await node.run(SKAction.scale(by: 25, duration: 6)) }

...but that seems even more cumbersome.

So - the bottom line - how do you indicate in Swift, when using structured concurrency and there is both a sync and async version of a function that you want to use the sync version?


Solution

  • Inside an async function, the compiler prefers the async variants of overloaded methods. There are a couple of ways to convince it to use the non-async function.

    Let‘s say we have an overloaded function foo:

    func foo() {}
    func foo() async {}
    

    The first way to choose the synchronous function is a closure. Inside the closure, the compiler prefers the call to the synchronous function:

    func barl() async {
        { foo() }()
    }
    

    We can also specify which function we want using as:

    func bar2() async {
        (foo as () -> _)()
    }
    

    For the sake of completeness, we can also use a closure to choose the synchronous function, then call the closure to get the function, then call the function:

    func bar3() async {
        { foo }()()
    }
    

    I would prefer the first solution, as the other two lose named parameters on the call. We can generalize it to a helper function:

    func sync<T>(block: () -> T) -> T {
        block()
    }
    

    With this helper, we can make explicit we are requiring a synchronous call:

    func bar() async {
        sync { foo() }
    }