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):
Referencing initializer 'init(priority:operation:)' on 'Task' requires the types 'Failure' and 'any Error' be equivalent
'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.
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
isNever
andFailure
isNever
.
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.