actor BetterInt: ExpressibleByIntegerLiteral {
private(set) var value: Int = 0
init(integerLiteral value: Int) {
self.value = value
}
}
will report error on init: Non-Sendable parameter type 'Self.IntegerLiteralType' cannot be sent from caller of protocol requirement 'init(integerLiteral:)' into nonisolated implementation
So I add sending keyword after the init parameter
actor BetterInt: ExpressibleByIntegerLiteral {
private(set) var value: Int = 0
init(integerLiteral value: sending Int) {
self.value = value
}
}
will also report error, but this time report on conformance: Type 'BetterInt' does not conform to protocol 'ExpressibleByIntegerLiteral'
protocol P {
associatedtype T
init(value: T)
}
class Q {} // non sendable
actor BetterInt: P {
init(value: Q) {}
// ❌ Non-Sendable parameter type 'Self.T' cannot be sent from caller of protocol requirement 'init(value:)' into nonisolated implementation
}
Yet, if I remove conformance to P it compiles fine...
actor BetterInt {
init(value: Q) {} // ✅
}
Shouldn't both of these versions result into an error? Or both compile fine?
Of the various solutions posted, @preconcurrency ExpressibleByIntegerLiteral seems the least intrusive. The preconcurrency attribute is a way to signal other developers looking at the code that you've manually verified that the protocol is thread-safe, or that you've worked around any threading issues. Since your conformance consists of an initializer with an Int parameter and has no isolation requirements, it should be safe.
actor BetterInt: ExpressibleByIntegerLiteral {
init (integerLiteral value: Int) {
^ Non-Sendable parameter type 'Self.IntegerLiteralType' cannot be sent from caller of protocol requirement 'init(integerLiteral:)' into nonisolated implementation
}
}
That error baffles me.
Isn't a value of type Int intrinsically sendable? What exactly is the concurrency issue here?
Technically what the compiler is saying here is that the associated type IntegerLiteralType of protocol ExpressibleByIntegerLiteral isn't constrained to be Sendable. The only requirements of the IntegerLiteralType type parameter is that it must conform to _ExpressibleByBuiltinIntegerLiteral (which doesn't require Sendable either).
In practice, only the stdlib's integer types can be used for the IntegerLiteralType type parameter (as only stdlib types can conform to _ExpressibleByBuiltinIntegerLiteral), and all of them are Sendable. But the protocol definition theoretically allows for the introduction of a non-sendable conformer to _ExpressibleByBuiltinIntegerLiteral (in which case, the above diagnostic would be correct!).
It doesn't seem very useful to have a non-sendable integer, though. Maybe _ExpressibleByBuiltinIntegerLiteral is just missing a Sendable annotation?
I think this really getting at the core of the issue. The compiler is looking at the protocol alone. And it is correct that a type could be used that is unsafe. It is just not taking in account the specific type of being used in this conformance.
This is an artificial limitation. This error is protecting against a situation that is impossible. But I do not know what the implications or difficultly level would be in changing this.
Personally, I think that @frogcjn ‘s solution 3 is the most reasonable, even though I agree it is tricky. Making actors conform to protocols with synchronous requirements is generally a difficult thing to do. But I like this option best, because it is truthfully expressing what you really want. It’s just a shame it needs to be done at all.
Not certain, but I think this has to do with the “special rule” applied to actor initializers:
Actor initializers have a special rule that requires their parameter values to be sent into the actor instance's isolation region. Actor initializers arenonisolated, so a call to an actor initializer does not cross an isolation boundary, meaning the argument values would be usable in the caller after the initializer returns under the standard region isolation rules. SE-0414 consider actor initializer parameters as being sent into the actor's region to allow initializing actor-isolated state with those values
Upon doing more research here, I made a bizarre discovery. I had the same problem, so I attempted to use the same solution of an intermediary protocol. And it did not work.
I’ve narrowed the problem down and it is kind of incredible. The protocol trick only works a) when it is explicitly nonisolated and b) only for init requirements. I cannot find any other combination where it will successfully compile.
// with inits
protocol InitRequirement {
associatedtype Value
init(_ v: Value)
}
actor BadInit: InitRequirement {
init(_ v: Int) { // error
}
}
protocol SendableInitRequirement: InitRequirement where Value: Sendable {}
actor Bad2Init: SendableInitRequirement {
init(_ v: Int) { // error
}
}
nonisolated protocol NonisolatedSendableInitRequirement: InitRequirement where Value: Sendable {}
actor GoodInit: NonisolatedSendableInitRequirement {
init(_ v: Int) { // works???
}
}
// with anything else
protocol NoninitRequirement {
associatedtype Value
func function(_ v: Value) async
}
actor BadFunction: NoninitRequirement {
func function(_ v: Int) async { // error
}
}
nonisolated protocol NonisolatedSendableNoninitRequirement: NoninitRequirement where Value: Sendable {}
actor GoodFunction: NonisolatedSendableNoninitRequirement {
func function(_ v: Int) async { // error???
}
}
Honestly, I’m amazed this bug was even found. The circumstances required to stumble upon a working version of this are impressive.
Anyways, the short answer is that I think an @preconcurrency conformance is the only viable and general-purpose workaround. I really don’t like it, and I have not fully validated that the runtime behavior is desirable, because this does insert runtime isolation checks. But at least it compiles.
(And even at that, I think the protocol definition must live in a different module for a @preconcurrency conformance to be usable!)
Edit: Actually no, the issue is the protocol must not use concurrency features. Doing that makes it ineligible for a @preconcurrency conformance. In that case I have not yet been able to find a workaround.
The intermediary protocol trick does not work, and neither does a @preconcurrency conformance. Anyone have any ideas that do not involve making Value sendable?