I've run into an odd compiler behaviour that disallows what seems to be a valid code, but doesn't compile. I'm using Xcode 16 beta 6 at the moment.
There is a protocol that looks like this:
protocol Command<In, Out> {
associatedtype In
associatedtype Out
func execute(_ input: In) async throws -> Out
}
It was Sendable
before, but I've revisited design and currently large amount of implementation use non-Sendable
types (and those cannot & shouldn't be such), with hope to rely on region-based isolation, since commands are meant to be created, executed, and forgotten.
So now I have the following code (simplified), that I think should work, but compiler gives me an error:
protocol CommandsFactory {
associatedtype LoggerCommandType: Command<String, Void>
func logger() -> sending LoggerCommandType
}
@MainActor
final class UseCase<Factory> where Factory: CommandsFactory {
private let factory: Factory
init(factory: Factory) {
self.factory = factory
}
func doSmth() async throws {
let command = factory.logger()
try await command.execute("did it!") // error: sending 'command' risks causing data races
}
}
From my understanding, command
here in a clearly disconnected region, so it should be safe to use it.
Currently I blame it on protocol, because if I replace CommandsFactory
to be just struct, the error is gone. Is this a compiler bug or I'm missing something?
UPD. Seems related to this issue:
Examples a bit different, but probably underlying reasons are the same.
The following seems to be reported and confirmed to be a bug: Cannot using `sending` with `some` return value · Issue #74846 · swiftlang/swift · GitHub
Also, either with CommandsFactory
being a struct, or when I am going to implement protocol, having sending
requirement makes it impossible to use opaque type:
struct MyCommandsFactory: CommandsFactory {
// error: 'sending' may only be used on parameters and results
func logger() -> sending some Command<String, Void> {
LoggerCommand()
}
}
If I'd use concrete type instead of a protocol, it is fine even without sending
(I guess compiler can reason about that), and I have an option to return just LoggerCommand
explicitly, instead of using opaque type, but shouldn't this work? Or am I missing something?
UPD 2. I've found ever more odd workaround for now:
func doSmth() async throws {
let factory = factory // that piece is essential here
try await Task { // as well as wrapping in a Task
let command = factory.logger()
try await command.execute("did it!") // ok!
}.value
}
UPD 3. Finally, with command
being property, I have this workaround working, while in my understanding, this should be disallowed?
@MainActor
final class Wrapper<CommandType> where CommandType: Command<String, Void> {
private let command: CommandType
init(_ command: CommandType) {
self.command = command
}
func execute() async throws {
let command = command
try await Task {
try await command.execute("I'm wrapped!") // no error!
}.value
}
}
I mean, there is no guarantee that command
here is a value type, so let command = command
might not create a disconnected copy, giving a potential data race I guess?