Standard Library Preview Package
01 何が問題だったのか
Swift Evolution を通じて標準ライブラリに新しい API が提案され、受理されると、そのコードは次の Swift リリースに合わせて標準ライブラリに統合されます。しかしこの流れには、実運用のフィードバックが反映されづらいという課題がありました。
受理からリリースまでの空白
受理された API が実際にリリースされ、多くのユーザーに使われるようになるまでには長い時間がかかります。たとえば SE-0199(Bool.toggle() の追加)は、Swift 4.2 のリリースより約 6 か月前に受理されています。
swift.org から配布される開発版ツールチェインはあくまで試用向けで、本番のアプリケーションを出荷することはできません。Xcode のベータ期間は多少実運用に近い使い方ができますが、期間は短く、仮にフィードバックで問題が見つかったとしても、再度 Swift Evolution を通す必要があるなどリリースまでに反映することは困難です。加えて、partial sort や最小ヒープのような機能は、ベータ序盤に「たまたま必要」となることばかりでもなく、本当に役立つ検証がなされるのは往々にしてリリース後です。
リリース後の変更のハードルの高さ
いったん API が Swift リリースの一部として出荷されてしまうと、後から変更する際にはソース互換性と ABI 安定性という非常に高いハードルを越える必要があります。ソース互換性を壊す変更は厳しく制限され、ABI 安定性にいたっては技術的にそもそも変更不可能なケースさえあります。特殊化を前提とする標準ライブラリ型は、性能の都合で内部実装の一部を ABI として公開せざるを得ず、その結果として将来的な最適化や性能改善の余地が狭まってしまう問題もあります。
つまり「使い始めて初めて分かる問題」を修正する機会が、現状のプロセスではほとんど確保されていません。
標準ライブラリへのコントリビューションの障壁
標準ライブラリに変更を加えるには、LLVM・Clang・Swift コンパイラ自体を含むツールチェイン全体をビルドできる環境が必要で、誰にでも気軽にできる作業ではありません。Xcode や XCTest で標準ライブラリのコードをそのまま保守・テストすることも簡単ではなく、非公開機能や ABI 安定性にまつわる作法の知識も求められます。
特に、特殊化可能で ABI 安定なコレクション型を、将来の拡張余地も残しつつ実装するのは、単にソース互換性のある実装を書くのに比べて格段に難しく、どの内部詳細を後から変更してよいかを判断するのも容易ではありません。このため、アイデアや実装力があるコントリビュータでも、標準ライブラリへの提案に二の足を踏むことがありました。
02 どのように解決されるのか
受理された Swift Evolution の提案のうち、適切なものを 個別の SwiftPM パッケージ として公開し、さらにそれらをまとめて再エクスポートするアンブレラパッケージ SwiftPreview を提供します。これらは標準ライブラリに統合される前段の「preview パッケージ」として位置づけられ、早期の実運用と、そこから得られたフィードバックによる調整を可能にします。
以下、個別パッケージと SwiftPreview をまとめて「preview パッケージ」、Swift ツールチェインに同梱される標準ライブラリを「ライブラリ」と呼びます。
preview パッケージに載せる変更の範囲
preview パッケージに入るのは、言語機能の変更を伴わず、既存の標準ライブラリ型の内部実装やコンパイラの builtin に頼らなくても実装できる追加 API に限られます。具体的には次のようなものです。
- 標準ライブラリのプロトコルに対する extension として実装される新しいアルゴリズム
- 関数から返されるユーティリティ型(たとえば
.lazyが返すようなLazySequence系の型) - 新しいプロトコルと、それに対する標準ライブラリ型の適合
- sorted dictionary のような新しいコレクション型
- 遅延初期化用などのプロパティラッパー
一方、次のようなものは preview パッケージには載せません。
OptionalやErrorのように、言語機能と不可分な型- atomics や SIMD のように、builtin に頼るのが妥当な実装
- 他の型の内部実装にアクセスしないと性能が出せない実装
- プロトコルのカスタマイズポイント追加のように、パッケージからはそもそも行えない変更
どちらに倣うかが微妙なケース(たとえば extension メソッドをカスタマイズポイントにしたときの性能差が無視できない場合など)もあり、最終的にパッケージとして出すかどうかは、pitch とレビューでの議論の対象になります。
Swift Evolution プロセスへの影響
Swift Evolution のプロセス自体は、ほぼ従来どおりです。すべての標準ライブラリへの追加は、これまでと同じように pitch・discussion・implementation・proposal・decision のサイクルを経ます。
変わる点は次のとおりです。
- Proposal の実装は、
swift-evolution-stagingリポジトリへの pull request として提出します。レビュー開始前にそのブランチへマージされ、レビュー期間中に実際に使って検証できるようになります。 - レビューを通過すると、当該パッケージは独立したリポジトリに移動し、preview パッケージとして即座に一般利用可能になります。却下された場合は staging リポジトリから削除されます。
- パッケージでリリースされた API についてフィードバックから修正を入れたい場合は、通常の提案と同じく改めて Swift Evolution プロセスを通します。ただし、まだ Swift リリースに含まれていない API なので、ソース互換性を壊す変更のハードルは「新規提案と同じ」程度にとどまり、ライブラリに入ってからのような極端な高さにはなりません。
- メジャーリリースのブランチ直後に新機能を入れていくことが好まれます。実運用のフィードバックを得る時間を最大限確保するためです。
- 「とりあえず暫定で受けておく」という運用はしません。レビュー時には「そのままライブラリへ移行する」前提で判断し、移行時に内容を再検討するのは、フィードバックで明らかな問題が見つかった場合に限ります。
ライブラリへの移行とソース互換性
パッケージとして提供されていた機能が、後に標準ライブラリに移行したときの挙動は、型と関数で扱いが異なります。
型
preview パッケージのモジュール名は SwiftPreview や SE250_LeftPad のようにライブラリの Swift モジュールとは別です。そのため、パッケージが提供する型とライブラリが提供する同名の型は別物として扱われ、共存可能です。
preview パッケージをインポートしているソースファイルでは、パッケージ側の型が既定で優先されます。ライブラリ側の型を使いたいときは Swift. を明示的にプレフィックスとして付けます。これにより、ライブラリに型が追加されても、すでにパッケージ版を使っているコードはそのまま動き続けます。
ただしこの方式には欠点もあります。パッケージユーザーは、OS に組み込み済みの型を使うことによるコードサイズ削減の恩恵を受けられません(多くの標準ライブラリ型は汎用型で特殊化されるため、影響は限定的ですが)。また、ライブラリに入ったあとで他の型の内部を活用した最適化が行われても、それを受けられません。
そういうケースでライブラリ版を使いたくなった場合は、以下のように typealias を書くか、必要な箇所に Swift. をプレフィックスとして付けるだけで切り替えられます。
typealias TheType = Swift.TheType
関数
関数(メソッド)は、型のようには曖昧さを排除できません。myCollection.SwiftPreview.partialSort のような書き方を Swift はサポートしていないため、プロトコル extension の関数について「パッケージ版ではなくライブラリ版を選ぶ」といった指定はできません。
これに対応するため、標準ライブラリに関数を取り込む際には @_alwaysEmitIntoClient 属性(Swift 5.1 以降、重要なバグ修正の back-deploy のために使われてきた内部機能)の活用を想定しています。この属性が付いた関数は、呼び出し元のバイナリに実体が埋め込まれるため、古いプラットフォームへ実装を配送できます。#if compiler ディレクティブと組み合わせれば、新しいコンパイラでのみパッケージ側の実装を非推奨化し、自然にライブラリ版へ切り替えていくことができます。
型と関数の組み合わせ
myArray.lazy.map のように、関数が返す型が実際の機能を担うパターンでは、結果の型がパッケージ版かライブラリ版かに揃います。デフォルトではパッケージ版が選ばれるため、ライブラリ版を使いたい場合は型文脈で明示する必要があります。
SwiftPreview からの退役
SwiftPreview パッケージのサイズが肥大化しないよう、標準ライブラリへ移行した proposal パッケージは、移行から 1 年後 のバージョンで SwiftPreview から取り除かれます。個別の proposal パッケージ自体は残り続けるので、コンパイラを上げずに SwiftPreview だけ更新したいユーザーも、必要な個別パッケージを直接インポートして使い続けられます。
テスト
preview パッケージのテストには XCTest を採用します。標準ライブラリは StdlibUnittest と lit を組み合わせた独自のテスト構成を使っていますが、これは多くの開発者にとってなじみの薄いものです。XCTest なら Xcode との親和性もよく、コントリビューションの障壁を下げるという preview パッケージの目的にも沿います。ライブラリへ統合するタイミングで、必要に応じて lit 形式へ書き換えることになります。
StdlibUnittest が提供する crashLater や StdlibCollectionUnittest の checkCollection、最小コレクションのようなヘルパは、可能であれば XCTest 向けに移植するのが望ましい方向性です(これはこの提案の必須要件ではなく、最初にフル機能のコレクション型を preview パッケージに入れるタイミングで取り組める「good first issue」程度の位置づけです)。
GYB の不使用
preview パッケージでは gyb を使いません。条件付き適合や最適化の改善によって gyb が必要な場面は減っており、強力ではあるもののコードの可読性や Xcode との相性に難があり、「コントリビューションを簡単にする」という preview パッケージの目的に反するためです。
バージョニング
個別の proposal パッケージは semantic versioning に従います。受理された時点で 1.0.0 としてリリースされ、ソース互換性を壊す変更はメジャーバージョンの更新でのみ許容されます。
一方、アンブレラの SwiftPreview パッケージは、常に最新の proposal を再エクスポートし続ける性質上、ソース互換性を壊す変更が頻繁に発生します(ライブラリへの移行に伴う退役、パッケージ段階で別途入る破壊的変更など)。そのため、SwiftPreview のメジャーバージョンは 恒久的に 0 のままに据え置きます。マイナーバージョンの更新でソース互換性を壊す変更を含み得るという扱いです。パッチ更新は基本的にソース互換性を保ち、バグ修正に使われます。
メジャーバージョン 0 の固定は「常に最新版に追従する心構えが必要」というシグナルです。プロダクションコードで使ってはいけないという意味ではなく、実運用に耐える品質ではあるものの preview であってソース安定ではない、ということを示すマーカーとして機能します。マイナーバージョンを切るタイミングは、主に proposal パッケージが追加・削除されるときで、標準ライブラリのリリースマネージャが判断します。
ABI 安定性への影響
preview パッケージは ABI 安定ではありません。そのため、バイナリフレームワークからの利用はできません。ABI 安定性は、ライブラリへ移行したタイミングで初めて確立されます。
モジュール横断の最適化がまだ存在しないため、パッケージの実装ではソース安定性の範囲で @inlinable を活用することが推奨されます。ただしこれらの @inlinable 指定は、ライブラリへ統合する段階で改めて見直す前提のものです。