Swift Digest
SE-0358 | Swift Evolution

Primary Associated Types in the Standard Library

Proposal
SE-0358
Authors
Karoy Lorentey
Review Manager
John McCall
Status
Implemented (Swift 5.7)

01 何が問題だったのか

SE-0346 により、プロトコル側で primary associated type を宣言しておけば、some Collection<Int> のように具象ジェネリック型と同じ軽量な構文で主要な associated type を固定できるようになりました。しかし、言語機能としての土台が整っても、それを支える標準ライブラリ側のプロトコルに primary associated type の宣言が無ければ、CollectionSequenceIdentifiable といった日常的に使うプロトコルに対してこの構文は使えません。

たとえば、String を要素とするシーケンスを受け取る関数は、従来どおり where 句で書かなければなりませんでした。

func concatenate<S: Sequence>(_ lhs: S, _ rhs: S) -> S where S.Element == String { ... }

また、opaque result type に対しては where 句を書く場所が無いため、要素型を公開する手段そのものが存在せず、次のような戻り値型では呼び出し側は要素型を知ることができませんでした。

func readSyntaxHighlightedLines(_ file: String) -> some Sequence { ... }

本 Proposal は、SE-0346 の軽量な制約構文を標準ライブラリのプロトコルでも使えるようにするため、どのプロトコルのどの associated type を primary にするかを洗い出して実際に宣言を追加することを目的としています。あわせて、primary associated type の選び方についての設計ガイドラインも示されます。

02 どのように解決されるのか

標準ライブラリの主要なプロトコルに primary associated type を追加します。これにより、some Collection<Int>some Identifiable<UUID> のような軽量な制約構文が、標準ライブラリのプロトコルに対しても使えるようになります。

追加される primary associated type

シーケンス・コレクション系には Element が、識別やラップ系には対応する型が primary associated type として宣言されます。

public protocol Sequence<Element>
public protocol IteratorProtocol<Element>
public protocol Collection<Element>: Sequence
public protocol MutableCollection<Element>: Collection
public protocol BidirectionalCollection<Element>: Collection
public protocol RandomAccessCollection<Element>: BidirectionalCollection
public protocol RangeReplaceableCollection<Element>: Collection

public protocol Identifiable<ID>
public protocol RawRepresentable<RawValue>
public protocol RangeExpression<Bound>
public protocol Strideable<Stride>: Comparable

public protocol SetAlgebra<Element>: Equatable, ExpressibleByArrayLiteral

public protocol SIMD<Scalar>: ...

public protocol Clock<Duration>: Sendable
public protocol InstantProtocol<Duration>: Comparable, Hashable, Sendable

これにより、次のようにプロトコル適合要件を書けるあらゆる位置で、具象ジェネリック型と同じ感覚で書けるようになります。

func concatenate<S: Sequence<String>>(_ lhs: S, _ rhs: S) -> S { ... }

func sortLines(_ lines: some Collection<String>) { ... }

func readSyntaxHighlightedLines(_ file: String) -> some Sequence<[Token]> { ... }

extension Collection<String> { ... }

Clock については Instant ではなく Duration が primary に選ばれています。実際の用途では「経過時間を物理的な秒で測るあらゆる Clock」を some Clock<Duration> と書けることのほうが、特定の Instant を固定するより有用だという判断によるものです(some Clock<ContinuousClock.Instant> は実質 ContinuousClock を遠回しに書いたものに等しくなってしまいます)。

primary associated type を持たないプロトコル

NumericSignedNumericBinaryIntegerFloatingPoint などの数値系プロトコルでは、Magnitude のような associated type は存在するものの、some Numeric<Int>Int が何を意味するか直感的に読み取りづらいため、あえて primary associated type は宣言されません。同様の理由で、ExpressibleBy...Literal 系や CaseIterableUnicode.EncodingStringProtocol なども primary associated type を持ちません。

OptionSetElement が常に Self になる設計なので RawValue が候補にはなるものの、混乱を避けるため primary associated type は付けない判断になりました。LazySequenceProtocol / LazyCollectionProtocol は、ElementElements のどちらを primary にすべきか判断が難しく、実運用の知見が溜まるまで保留とされています。

AsyncSequence と分散アクター系の先送り

AsyncSequenceAsyncIteratorProtocol は論理的には Element を primary にすべきですが、別途議論されている「正確なエラー型を付ける」拡張が実現すると、Error も同時に primary としたくなる可能性があります。primary associated type の追加・並び順変更は source-breaking になるため、確定するまで primary 宣言は見送られています。

DistributedActorDistributedActorSystem などの分散アクター系プロトコルも、分散アクター機能側の今後の言語拡張と絡むため、本 Proposal では primary associated type を付けずに将来の Proposal に委ねられています。

primary associated type を選ぶときの指針

本 Proposal では、標準ライブラリでの採否を検討するにあたり用いられたガイドラインも、参考情報として共有されています。自分のプロトコルに primary associated type を付けるかどうか迷ったときの出発点として有用です。

  • 利用実態に合わせる。既存プロトコルなら、where 句で実際によく制約される associated type を選ぶ。たとえば Sequence の利用箇所では Iterator はまず制約されず、Element が圧倒的に多いため、Element が自然な選択になります。
  • 利用側の可読性を重視するsome Bar<Character>Character が何の役割を持つか、プロトコルを知る人が直感的に読み取れる必要があります。Collection of IntIdentifiable by String のように、単純な前置詞で関係を言い表せる associated type は primary に向きます。逆に NumericMagnitude のように役割が微妙なものは不向きです。
  • 無理に付けない。制約されることがあまり想定されないなら、primary 宣言しないほうが安全です。
  • 原則として一つに絞る。SE-0346 では、軽量構文を使うときに primary associated type を一つだけ省略するような書き方ができないため、複数宣言すると一部だけ制約したい場面で従来のジェネリック構文に戻らざるを得なくなります。両方の associated type がほぼ必ず一緒に制約される場合に限り、複数宣言を検討します。

いったん primary associated type を宣言すると、削除や並び順の変更、さらに(SE-0346 の現状の制約下では)追加も source-breaking な変更になります。そのため、本 Proposal で宣言された primary associated type のリストも、以後の変更は基本的に行えないものとして扱われます。