この記事の要点
AsyncSequence向けのアルゴリズム群をまとめた新しいオープンソースパッケージ Swift Async Algorithms(swift-async-algorithms)が発表されました。async/awaitとの一級の統合、時間(time)を扱うアルゴリズムの提供、クロスプラットフォームかつオープンソースであることを目標としています。- 時間とともに流れる値(values over time) を扱うことに焦点を当てており、
zip・combineLatest・merge・chainのような複数の入力を合成するアルゴリズムや、debounce・throttleのような時間に基づくアルゴリズムを提供します。 - Apple の Combine フレームワークでの知見を踏まえ、structured concurrency を活かして、宣言的な演算子の連鎖ではなく上から下へ読めるコードでこの種のロジックを書けるようにすることを狙っています。発表時点ではプロトタイプ版で、今後 Swift フォーラムで設計議論を進めていく位置づけです。
何が発表されたのか
Swift Async Algorithms は、AsyncSequence を対象としたアルゴリズム群を提供するパッケージです。Swift が進めてきた、安全・シンプル・高性能な非同期プログラミングの取り組みの一環として位置づけられています。
このパッケージは、時間とともに流れる値 を扱うアルゴリズムを対象とします。debounce や throttle のように主に 時間 を扱うものだけでなく、combineLatest や merge のように値の 順序 を扱うものも含みます。zip が Sequence に対して行うような、複数の入力を扱う操作は、微妙な挙動や考慮すべきエッジケースが多く、実装が驚くほど複雑になりがちです。共有パッケージとしてこうした細部を正しく作り込み、十分なテストとドキュメントを備えることで、すべての Swift アプリがその恩恵を受けられるようにすることが狙いです。
土台となるのは Swift 5.5 で導入された AsyncSequence です。Swift 5.5 では、AsyncSequence の値を自然な for/in ループと await で処理できるようになり、map や filter のような Sequence 相当の API も使えます。structured concurrency によって、中間状態を単なるローカル変数として扱い、throw する関数に直接 try を使い、非同期コードのロジックを同期コードと同じように書けるようになりました。
開発と API 設計は GitHub と Swift フォーラム 上で行われます。パッケージとして提供することで、プラットフォームや OS バージョンをまたいだ柔軟なデプロイが可能になります。
何に使えるのか
パッケージには、おなじみのアルゴリズムの AsyncSequence 版として、zip・combineLatest・merge・chain・buffer・debounce・throttle などが含まれます。
複数の AsyncSequence を合成する
zip は Sequence 版と同じく、2 つの異なる AsyncSequence の値からタプルを作ります。
for await (number, letter) in zip(numbers, letters) {
print(number, letter)
}
AsyncSequence は rethrows をサポートしており、エラー処理はほかの Swift コードと同じく try を使うだけです。
do {
for try await (number, letter) in zip(numbers, lettersWithErrors) {
print(number, letter)
}
} catch {
// Handle error
}
combineLatest や merge も複数の AsyncSequence の出力を合成しますが、出力の型やタイミングの制御の仕方がそれぞれ異なります。
時間を扱う
Sequence と AsyncSequence の根本的な違いは、時間 という変数が加わる点です。Clock と Duration を標準化する Proposal を土台として、このパッケージは debounce や throttle といったアルゴリズムを追加します。速すぎる間隔で到着する値を捨てる、といったよくある操作を手軽に実現できます。
for await value in input.debounce(for: .seconds(0.5)) {
// Handle input, at most once per 0.5 seconds.
}
値の収集と同期シーケンスとの連携
有限の非同期シーケンスのすべての値を集めたい場合、1 行で書ける初期化子が用意されています。
let result = await Array(input)
async 関数を使うと、同期的な Sequence を AsyncSequence と組み合わせられます。次の例では chain 関数と併用して、ファイルの内容の前に前置きを付け加えています。
let preamble = [
"// This source file is part of the Swift.org open source project",
"//",
""
].async
let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines)
for try await line in lines {
print(line)
}
Combine との関係
Apple は iOS 13・macOS 10.15 の SDK で Combine フレームワークを導入しました。Combine の API は Publisher と Subscriber というインターフェイスを基盤とし、両者をつなぐ 演算子(operator) を組み合わせます。データが一方から他方へ流れる間に変換していく演算子の連鎖を宣言的に記述する設計です。
この設計では中間状態を異なる形で捉える必要があり、特に単一の値・エラー・共有が必要なデータを扱うとき、呼び出し側のコードが想像以上に複雑になることがあります。async/await の structured concurrency は、この種のロジックを表現する新しい方法を提供します。変換の連鎖ではなく、小さな部品に分割され上から下へ読める非同期コードとして書けるようになります。
Swift Async Algorithms は、Combine の実運用から得た教訓を取り入れつつ、Swift の structured concurrency を活かすパッケージとして設計されています。
導入・今後の位置づけ
発表時点で示されていた今後の構想であり、実現を約束するものではありません。
発表時点で公開されたのはプロトタイプ版です。まず動作する実装でプロジェクトを始動させ、その後 Swift フォーラムで詳細な設計議論を進めていく方針が示されました。次のような形でのコミュニティの参加が呼びかけられています。
- パッケージの早期採用と設計へのフィードバック
- パッケージ本体の実装
- テストの実装
- 今後の機能の pitch と evolution
バグ・機能要望・着手しやすいタスクの管理には GitHub Issues が使われます。async/await と AsyncSequence がもたらす可能性を活かし、この領域のより高レベルな API の将来の開発と evolution を探っていく場として位置づけられています。