Inferring Sendable for methods and key path literals
01 何が問題だったのか
Swiftでは、メソッドを呼び出さずに参照するだけで関数値を作ることができます。インスタンスに束縛した形(部分適用)と、型に対して束縛していない形(unapplied)の二通りがあります。
struct S {
func f() { /* ... */ }
}
let partial: () -> Void = S().f // 部分適用
let unapplied: (S) -> (() -> Void) = S.f // unapplied
ところが、これらの関数値を @Sendable が要求される場所に渡そうとすると、型が Sendable であっても @Sendable 性が自動では伝わらず、警告になっていました。
protocol P: Sendable { init() }
func g<T: P>(_ f: @escaping @Sendable (T) -> (() -> Void)) {
Task { f(T())() }
}
struct S: P {
func f() {}
}
g(S.f) // warning: Converting non-sendable function value to
// '@Sendable (S) -> (() -> Void)' may introduce data races
メソッドが実際にキャプチャするのは self のインスタンスだけなので、S が Sendable であれば S.f も S().f も本来 @Sendable として安全に扱えるはずです。しかし従来はそれをユーザー側で明示しなければならず、g({ @Sendable in S.f($0) }) のように @Sendable クロージャでラップするボイラープレートが必要でした。
key path リテラルの過剰な制約
SE-0302 では、key path リテラルは 常に暗黙に Sendable として扱われる、と定められていました。その結果、subscript の引数などに non-Sendable な値をキャプチャする key path を書くと、たとえ並行に渡す予定がなくても警告が出てしまいます。
class Info: Hashable { /* ... */ }
public struct Entry {}
public struct User {
public subscript(info: Info) -> Entry { /* ... */ }
}
let entry: KeyPath<User, Entry> = \.[Info()]
// warning: cannot form key path that captures non-sendable type 'Info'
このコード自体は isolation boundary を越えないためデータ競合は起こらないのに、key path を作った瞬間に警告が出るのは過剰です。別モジュールで定義された型については対処のしようもなく、また、並行性をまだ意識していない利用者にとっては唐突に感じられる診断であり、progressive disclosure の観点からも望ましくありません。
グローバル関数の扱い
クロージャと違い、グローバル関数やstaticなグローバル関数は何もキャプチャしません。それでも、Task に直接渡すと「@Sendable ではない」として警告になっていました。
func doWork() -> Int { Int.random(in: 1..<42) }
Task<Int, Never>.detached(priority: nil, operation: doWork)
// warning: Converting non-sendable function value to
// '@Sendable () async -> Void' may introduce data races
キャプチャが存在しない以上、データ競合の余地はなく、既定で @Sendable にできるはずです。
non-Sendable 型のメソッドに @Sendable を付けられてしまう問題
逆方向の穴もありました。Sendable ではない型のメソッドに @Sendable を付けると、そのメソッドを通して内部のミュータブルな状態が並行に触られ、データ競合を招きかねません。
class C {
var random: Int = 0 // `C` は Sendable にできない
@Sendable func generateN() async -> Int { // 本来は禁止されるべき
random = Int.random(in: 1..<100)
return random
}
}
この書き方は現状コンパイルが通ってしまい、@Sendable 属性の意味を壊していました。
02 どのように解決されるのか
関数値と key path リテラルについて、「non-Sendable な値をキャプチャしない」ことがコンパイラにわかるケースでは @Sendable / & Sendable を 自動的に推論 するようにします。あわせて、@Sendable を付けてはいけないケースを明示的に禁止します。
本提案は upcoming feature flag InferSendableFromCaptures で有効化できます。
1. Sendable 型の unapplied メソッドを @Sendable として推論
型が Sendable に適合していれば、そのメソッドの unapplied 参照は @Sendable 関数型として扱えます。
struct User: Sendable {
var address: String
var password: String
func changeAddress(new: String, old: String) { /* ... */ }
}
let unapplied: @Sendable (User) -> ((String, String) -> Void) = User.changeAddress // OK
2. Sendable 型の部分適用メソッドを @Sendable として推論
部分適用(インスタンスに束縛した)メソッドの関数値も、インスタンスの型が Sendable であれば @Sendable として扱えます。
let partial: @Sendable (String, String) -> Void = User().changeAddress // OK
ただし、mutating メソッドの unapplied / 部分適用参照はもともと言語仕様上認められていないため(SE-0042 参照)、この推論の対象外です。staticメソッドは対象に含まれます。
3. key path リテラルは「non-Sendable がデフォルト」に転換し、必要なときに & Sendable を推論
key path リテラルの扱いを反転させ、明示的に & Sendable を要求しない限り、key path は non-Sendable とします。そのうえで、non-Sendable な値をキャプチャせず、actor-isolated なプロパティ/subscript も参照していない key path については、文脈から & Sendable を推論します。
struct User {
var name: String
@MainActor var age: Int
subscript(_ info: Info) -> Entry { /* ... */ }
}
let name = \User.name // WritableKeyPath<User, String> & Sendable(推論)
let ageKP = \User.age // KeyPath<User, Int>(non-Sendable: age が MainActor isolated)
let infoKP = \User.[Info()] // non-Sendable(Info が non-Sendable)
明示的に型を書いた場合の扱いは次のとおりです。
let name: KeyPath<User, String> & Sendable = \.name // OK
let name: KeyPath<User, String> = \.name // OK だが non-Sendable
let otherName: KeyPath<User, String> & Sendable = name // エラー(name は non-Sendable)
let entry: KeyPath<User, Entry> & Sendable = \.[Info()] // エラー(Info が non-Sendable)
key path 型は @Sendable 関数型と相互に使えます。@Sendable (User) -> String を要求する文脈に \.name を渡すと、コンパイラが { $0[keyPath: \.name] } に相当するクロージャを合成します。
let name: @Sendable (User) -> String = \.name // OK(\.name 自身が Sendable である必要はない)
let value = NonSendable()
let _: @Sendable (User) -> String = \.[value] // エラー(value をキャプチャしてしまう)
KeyPath を引数に取る既存APIは、@preconcurrency を付けつつ & Sendable を足すことでABI互換を保ったまま要件を強化できます。
@preconcurrency public func getValue<T, U>(_: KeyPath<T, U> & Sendable) { /* ... */ }
key path リテラルが引数として渡される場合は、パラメータ側の & Sendable 要件が推論のヒントになります。
func getValue<T: Sendable>(_: KeyPath<User, T> & Sendable) -> T {}
getValue(\.name) // OK(& Sendable が推論される)
getValue(\.[NonSendable()]) // エラー(non-Sendable なキャプチャ)
4. 非ローカル関数は既定で @Sendable
グローバル関数とstaticなグローバル関数は何もキャプチャしないため、既定で @Sendable として扱われます。
func doWork() -> Int { Int.random(in: 1..<42) }
Task<Int, Never>.detached(priority: nil, operation: doWork) // OK
5. non-Sendable 型のメソッドへの @Sendable 付与を禁止
型自身が Sendable でないのに、そのメソッドだけ @Sendable を名乗ることは data race を招きうるため、コンパイラが拒否します。
class C {
var random: Int = 0
@Sendable func generateN() async -> Int { // error: adding @Sendable to function
// of non-Sendable type prohibited
random = Int.random(in: 1..<100)
return random
}
}
本提案の推論が入った結果、利用者はメソッド/関数宣言に @Sendable を明示的に書く必要がなくなります。既存の @Sendable 付き宣言は引き続き許容されますが、役割は実質的に推論に置き換わります。
appending(...) の Sendable 版オーバーロード
KeyPath.appending(...) は、ベースと引数がともに & Sendable な場合に結果も & Sendable になるよう、Sendable 拡張として新しいオーバーロードが標準ライブラリに追加されます。
extension Sendable where Self: AnyKeyPath {
@inlinable
public func appending<Root, Value, AppendedValue>(
path: KeyPath<Value, AppendedValue> & Sendable
) -> KeyPath<Root, AppendedValue> & Sendable where Self: KeyPath<Root, Value> {
// ...
}
}
func makeUTF8CountKeyPath<Root>(
from base: KeyPath<Root, String> & Sendable
) -> KeyPath<Root, Int> & Sendable {
return base.appending(path: \.utf8.count) // OK: 両方 Sendable なので結果も Sendable
}
互換性への影響
型を 明示していない 宣言で、関数値や key path リテラルが Sendable 性を新たに獲得するため、Sendable 有無でオーバーロードされているAPIの解決が変わることがあります。
func callback(_: @Sendable () -> Void) {}
func callback(_: () -> Void) {}
callback(MyType.f) // f が @Sendable 推論されるなら最初のオーバーロードが選ばれる
func getValue(_: KeyPath<String, Int> & Sendable) {}
func getValue(_: KeyPath<String, Int>) {}
getValue(\.utf8.count) // & Sendable 版が選ばれる
また、宣言から @Sendable を外して推論に任せると、そのメソッドのマングリングが変わる点には注意が必要です(Sendable はmarker protocolなので & Sendable の付け外し自体はABI中立です)。
Future Directions
アクセサ(getter / setter)は今回のスコープに含まれていません。将来的には、少なくとも getter に対して同じ仕組みで @Sendable 推論を及ぼす拡張が考えられます(speculativeであり、実現を約束するものではありません)。