Question

How to add static functions that use Tasks in Swift Task extensions

I'm curious if anyone has run into this question before. I initially ran into problems when trying to use an extension to Task that calls Task.sleep from a non-async function static function, then further digging has led me to this simpler discussion point.

This is valid Swift:

struct Foo {}

extension Foo {
    static func bar() async throws {
    }
    
    static func bar() {
        Task {
            try await bar()
        }
    }
}

But the following is not:

extension Task {
    static func bar() async throws {
    }
    
    static func bar() {
        Task {
            try await bar()
        }
    }
}

This gives me two errors (in Xcode 15.4):

  1. Referencing initializer 'init(priority:operation:)' on 'Task' requires the types 'Failure' and 'any Error' be equivalent

  2. 'Cannot convert value of type '()' to closure result type 'Success'.

Why is the compiler treating the Task extension differently, and how do we solve this? I know that Success and Failure are two placeholder types for the Task generic, but I don't think they should be affecting the Task instance in the bar static function's implementation.

 2  59  2
1 Jan 1970

Solution

 1

When you write the name of the extended type in an extension, without any type parameters, it is assumed that you mean to use already-declared type parameters. After all, this is what happens in the type's own declaration:

struct Foo<T> {
    func foo() {
        let x = Foo() // "Foo()" means "Foo<T>()"
    }
    
    var bar: Foo { // this means "var bar: Foo<T>"
        Foo()
    }
}

In an extension, it is "as if" you are in the type's declaration

extension Set {
    func foo() {
        let x = Set() // "Set()" means "Set<Element>()"
    }
    
    var bar: Set { // this means "var bar: Set<Element>"
        []
    }
}

So in your case, it assumes that you mean Task<Success, Failure> { ... }, i.e. you are creating a task that returns whatever type the caller wants, and can throw whatever type of error the caller wants to throw. This is obviously not what you want.

You should add constraints to Success and Failure:

extension Task where Success == Void, Failure == any Error {
    static func bar() async throws {
    }
    
    static func bar() {
        Task {
            try await bar()

            // as discussed below, Task.sleep requires that Success == Never, Failure == Never
            // but writing 'Task' on its own here would mean Task<Void, any Error>
            try await Task<Never, Never>.sleep(...)
        }
    }
}

It is also possible to directly write the type parameters out, without adding the constraints to Success and Failure

Task<Void, any Error> {
    try await bar()
    try await Task<Never, Never>.sleep(...)
}

However, this makes the call site more cumbersome - you cannot just write:

Task.bar()

because Task needs two type parameters and the compiler cannot infer them.

Notice how other static methods on Task also constrains the Success and Failure types. e.g. the documentation for sleep says:

Available when Success is Never and Failure is Never.

This is to help the compiler infer the type parameters so that the caller can just write Task.sleep(...) instead of Task<Never, Never>.sleep(...). The exact type that Success and Failure are constrained to isn't very important.

2024-07-21
Sweeper