Allow TaskGroup’s ChildTaskResult Type To Be Inferred
01 何が問題だったのか
withTaskGroup(of:returning:body:) および withThrowingTaskGroup(of:returning:body:) で TaskGroup / ThrowingTaskGroup を作る際、ジェネリックパラメータのうち子タスクの結果型 ChildTaskResult は必ず of: 引数で明示的に指定する必要がありました。
let messages = await withTaskGroup(of: Message.self) { group in
for id in ids {
group.addTask { await downloadMessage(for: id) }
}
var messages: [Message] = []
for await message in group {
messages.append(message)
}
return messages
}
withTaskGroup にはもう一つ GroupResult というジェネリックパラメータがあり、こちらはクロージャの戻り値から多くの場合で推論されます(上の例では [Message])。しかし ChildTaskResult は addTask(...) に渡すクロージャの戻り値から推論されず、初学者にとっては ChildTaskResult と GroupResult の違いや、どちらにどの型を書くべきかが直感的ではありませんでした。
特に子タスクの結果が Void の場合、次のように of: Void.self を明示しなければならず、なぜこれが必要なのかが分かりづらいという問題がありました。
let logCount = await withTaskGroup(of: Void.self) { group in
for id in ids {
group.addTask { await logMessageReceived(for: id) }
}
return ids.count
}
なお、子タスクの結果を破棄する withDiscardingTaskGroup(returning:body:) と withThrowingDiscardingTaskGroup(returning:body:) は、そもそも子タスクが常に Void を返す前提なので ChildTaskResult ジェネリックを持っておらず、この問題は発生しません。
02 どのように解決されるのか
withTaskGroup(of:returning:body:) と withThrowingTaskGroup(of:returning:body:) の of childTaskResultType: ChildTaskResult.Type 引数にデフォルト値 ChildTaskResult.self を追加し、ChildTaskResult も GroupResult と同様にクロージャからの推論に委ねられるようにします。
public func withTaskGroup<ChildTaskResult, GroupResult>(
of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, // 追加されたデフォルト値
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable
これは SE-0326 による複数文クロージャの引数・戻り値の型推論の改善により可能になったものです。これを利用すると、冒頭の例は次のように簡潔に書けるようになります。
// `(of: Message.self)` を書かなくてよい
let messages = await withTaskGroup { group in
for id in ids {
group.addTask { await downloadMessage(for: id) }
}
var messages: [Message] = []
for await message in group {
messages.append(message)
}
return messages
}
ここでは ChildTaskResult が Message に、GroupResult が [Message] に推論されます。子タスクの結果が Void のケースも同様で、of: Void.self を書く必要がなくなります。
let logCount = await withTaskGroup { group in
for id in ids {
group.addTask { await logMessageReceived(for: id) }
}
return ids.count
}
推論の仕組みと注意点
型推論は top-down で行われ、クロージャの中で group を最初に使う文から ChildTaskResult が決まります。そのため、addTask(...) 以外の呼び出しが先に来ると推論に失敗することがあります。
// `ChildTaskResult` を `Void` にしたい想定
await withTaskGroup { group in // エラー: Generic parameter 'ChildTaskResult' could not be inferred
// `addTask(...)` より先に `group` を使っているため推論できない
group.cancelAll()
for id in ids {
group.addTask { await logMessageReceived(for: id) }
}
}
このような場合は、従来どおり of: で明示的に型を指定すれば解決します。
await withTaskGroup(of: Void.self) { group in
group.cancelAll()
for id in ids {
group.addTask { await logMessageReceived(for: id) }
}
}
もう一つ、addTask(...) のクロージャが異なる型を返している場合もエラーになります。この場合は最初の addTask(...) の戻り値から推論された型に後続が一致しないため、従来どおり 2 番目以降のクロージャに対してエラーが出ます。
await withTaskGroup { group in
group.addTask { await downloadMessage(for: id) }
// エラー: Cannot convert value of type 'Void' to closure result type 'Message'
group.addTask { await logMessageReceived(for: id) }
}
addTask(...) をタスクグループの最初の文にするという一般的な使い方であれば、これらの落とし穴に遭遇することはほとんどありません。型を明示したい場合はこれまでどおり of: を書けばよく、本提案は既存コードの互換性を損なわずに of: を省略可能にするものです。