it seems this was noticed nearly two years ago, and yet nothing has been done about it.
i guess this has something to do with keypaths capturing accessor parameters, and we have no way of representing the sendability of those parameters in KeyPath’s type signature. but what could we possibly use besides a KeyPath to do keypath things? (except for… escaping closures…?)
do we need SendableKeyPath, that can only capture Sendable accessor parameters?
Key paths themselves conform to the Sendable protocol. However, to ensure that it is safe to share key paths, key path literals can only capture values of types that conform to the Sendable protocol. This affects uses of subscripts in key paths:
class SomeClass: Hashable {
var value: Int
}
class SomeContainer {
var dict: [SomeClass : String]
}
let sc = SomeClass(...)
// error: capture of 'sc' in key path requires 'SomeClass' to conform
// to 'Sendable'
let keyPath = \SomeContainer.dict[sc]
But apparently that hasn't been implemented. That snippet doesn't produce the expected error.
There was some discussion about loosening this, and having sendable keypath literals produce KeyPath & Sendable, but AFAICT that never made it in to a proposal.
t.swift:20:36: warning: cannot form key path that captures non-sendable type 'SomeClass'
let keyPath = \SomeContainer.dict[sc]
^
Right. I continue to think this is the right way forward, because the currently set of restrictions makes it impossible to form key paths to actor-isolated state.
We're adopting Swift 6 language mode, and going through our code base to fix errors and warnings.
We have one issue involving key paths and this thread seemed the most relevant.
We're trying to cast a PartialKeyPath to a KeyPath so it can be used to build a predicate expression, but in the process the Sendable annotation is lost.
public protocol KeyPathProviding: Sendable {
static func makeKeyPath(_ identifier: String) throws -> PartialKeyPath<Self> & Sendable
static func makeExpression<T: Sendable>(
identifier: String,
_ input: PredicateExpressions.Variable<some Any>
) throws -> any PredicateExpression<T>
}
extension KeyPathProviding where Self: Sendable {
static func makeExpression<T: Sendable>(
identifier: String,
_ input: PredicateExpressions.Variable<Self>
) throws -> any PredicateExpression<T> {
let partialKeyPath = try makeKeyPath(identifier) // 'partialKeyPath' is 'any PartialKeyPath<Self> & Sendable'
guard let keyPath = partialKeyPath as? KeyPath<Self, T> else { // 'keyPath' is 'KeyPath<Self, T>'
fatalError()
}
func buildKeyPath(_ root: some PredicateExpression<Self>) -> any PredicateExpression<T> {
// error: Type 'KeyPath<Self, T>' does not conform to the 'Sendable' protocol
PredicateExpressions.build_KeyPath(root: root, keyPath: keyPath)
}
return buildKeyPath(input)
}
}
Adding extension KeyPath: @unchecked @retroactive Sendable {} fixes the error, but does not feel approriate.
Yeah, there are a few situations where key paths lose their sendability unfortunately (like appending). The only work around we have found is to just bit cast the key path to an & Sendable one when we "know" it's sendable:
Of course, if your key path is not sendable (like if it is a subscript key path that captures non-sendable data), then this will crash. So the responsibility is on you to make sure you are casting only when the key path is definitely sendable, which in your case I think it is ok since makeKeyPath returns a sendable key path.