Can you show the exact signature of the function that's making that complaint?
Remove @escaping
from my first code block above.
Oh wait, if I also remove the where
clause the error goes away. So I guess you're right...
The only two situations in the language where you get a non-escaping function type are if you either have a literal function type in parameter position, or a reference to a type alias with a function type in parameter position. Eg:
func foo(fn: () -> ()) {} // fn is non-escaping by default
typealias Fn = () -> ()
func foo(fn: Fn) {} // fn is non-escaping by default
In these cases, the compiler also allows you to state the @escaping
attribute.
This is similar to your example:
func g(_: @escaping () -> ()) {}
protocol P {
associatedtype A
}
struct S: P {
typealias A = () -> ()
}
struct G<T: P> {}
extension G where T == S {
func f(_ fn: @escaping T.A) {
g(fn)
}
}
Now, f()
in the extension references T.A
in parameter position. T
is concretely S
, so T.A
is interpreted as a concrete type reference S.A
. Since A
is a type alias with function type, T.A
resolves to a non-escaping function type under the current rules, and we have to apply @escaping
to pass the function to g()
.
However, if T.A
is not concretely known to be a function type, eg, if you instead had the following:
// g() is just generic now
func g<T>(_: T) {}
extension G where T: P { // no same-type requirement
func f(_ fn: T.A) {
g(fn)
}
}
Then @escaping
is an error when applied to T.A
, because it's just a type parameter. It is also not necessary, because an abstract type parameter cannot be non-escaping.
There's actually an inconsistency here, because T
in the below is never non-escaping:
struct G<T> {}
extension G where T == () -> () {
func f(_ fn: T) {
g(fn) // fine
}
}
But it can be explained away that T
itself is not a reference to a concrete type alias; it's still just a type parameter. (I think perhaps the interaction between @escaping
and type aliases is too magical, and we should always resolve type aliases as escaping function types, even when referenced from function parameter position. But that would be a breaking change that will require a language mode.)
In this case, the compiler can, fully statically, know that the argument to init(_:executing:)
is a function type and needs to be @escaping
because you've constrained Call == SyncCall
and SyncCall.Callback
is a function type.
Yeah, in retrospect it should have been obvious. When there's no indication at all that a parameter is going to be a function type, as in the non-specialized version of my initializer, then @escaping
doesn't make sense. I guess what I missed was that when it turns out to be a function type, then it's implicitly escaping.
Yeah, the formal restriction that makes this all work out is that non-escaping function types cannot be substituted for type parameters; you can’t pass them around or store them anywhere “generically”.
I think I narrowed the problem down to the minimum. It seems impossible to refer to an asynchronous typed throwing function, within a constrained function.
struct Container<Value> {
let value: Value
func getValue<Error>() -> Value
where Value == @Sendable () async throws(Error) -> Void {
value
}
func performValue<Error>() async throws(Error)
where Value == @Sendable () async throws(Error) -> Void {
try await value()
}
}
let container = Container { @Sendable () async in }
await container.value() // Works fine.
_ = container.getValue() // Command SwiftCompile failed with a nonzero exit code
await container.performValue() // Compiles, but never returns.