Swift Digest
SE-0499 | Swift Evolution

Support ~Copyable, ~Escapable in simple standard library protocols

Proposal
SE-0499
Authors
Ben Cohen
Review Manager
Holly Borla
Status
Implemented (Swift Next)

01 何が問題だったのか

SE-0427 によって non-copyable / non-escapable な型がジェネリクスに参加できるようになり、SE-0437Optional / Result やポインタ系のプリミティブも non-copyable 要素を扱えるようになりました。しかし、標準ライブラリの主要なプロトコル群は依然として「適合する型はコピー可能である」という前提で書かれており、non-copyable な型はそれらに適合できない状態でした。

具体的には、Equatable / Comparable / HashableCustomStringConvertible / CustomDebugStringConvertibleTextOutputStream / TextOutputStreamableLosslessStringConvertible といった、ごく基本的なプロトコルに non-copyable な型を適合させることができませんでした。これらのプロトコルは Swift コードのいたるところで前提とされており、たとえば ArrayUniqueArray のような non-copyable なコレクションで置き換えたいと思っても、置き換え先のコードが Hashable を要求していればその時点で成立しません。

加えて、OptionalResult は SE-0437 によって non-copyable な中身を持てるようにはなったものの、その条件付き Equatable / Hashable 適合は「中身が Copyable」を前提としたままでした。non-copyable な Optional<Wrapped> 同士を == で比較したり Dictionary のキーにしたりすることはできなかったわけです。

これらのプロトコルの要件はもともと単純で、引数を借用できれば十分なものがほとんどです。たとえば EquatableComparable は左右のオペランドを borrow できれば比較できますし、Hashable は borrow した値から Hasher にハッシュ値を畳み込むだけで事足ります。CustomStringConvertible.description のような文字列化も、値を借用して String を作れば済みます。つまり意味論上は non-copyable / non-escapable な型でも十分に実装可能であり、これらのプロトコルが従来のコピー可能性要件に縛られていたのは、言語機能が追いつく前の実装上の名残という側面が強いものでした。

この制約は、non-copyable な型を実用的に使ううえでの大きな障壁になっていました。non-copyable 性は、一意所有を強制して偶発的な共有を防ぐ用途だけでなく、String や任意精度数値型のような「参照カウントに頼らない効率的なデータ構造」を作るためにも使われます。後者の用途では、値同士を比較したりハッシュ化したりできることが非常に重要で、EquatableHashable に適合できないのは致命的でした。

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

標準ライブラリのいくつかの「シンプルな」プロトコルを ~Copyable / ~Escapable に一般化し、non-copyable / non-escapable な型もそれらに適合できるようにします。既存のセマンティクスは変わらず、適用可能な型の範囲が広がるだけです。

対象となるプロトコルは次のとおりです。

  • Equatable / Comparable / Hashable~Copyable, ~Escapable に一般化
  • CustomStringConvertible / CustomDebugStringConvertible~Copyable, ~Escapable に一般化
  • TextOutputStream / TextOutputStreamable~Copyable, ~Escapable に一般化
  • LosslessStringConvertible~Copyable にのみ一般化(~Escapable は含めない)

あわせて、OptionalResult の条件付き Equatable / Hashable 適合も、中身が non-copyable / non-escapable な場合まで拡張されます。

Equatable / Comparable / Hashable

Equatable は要件の == が左右のオペランドを borrow で受け取る形に整理され、プロトコル自身が ~Copyable, ~Escapable になります。ComparableHashable も同様に継承され、Hasher.combine も non-copyable / non-escapable な値を borrow で受け取れるようになります。

protocol Equatable: ~Copyable, ~Escapable {
  static func == (lhs: borrowing Self, rhs: borrowing Self) -> Bool
}

protocol Comparable: Equatable, ~Copyable, ~Escapable {
  static func < (lhs: borrowing Self, rhs: borrowing Self) -> Bool
  // <=, >=, > も同様に borrowing
}

protocol Hashable: Equatable & ~Copyable & ~Escapable { }

struct Hasher {
  mutating func combine<
    H: Hashable & ~Copyable & ~Escapable
  >(_ value: borrowing H)
}

これにより、たとえば non-copyable な文字列型や任意精度数値型を自前で定義して、==< で比較したり、Dictionary / Set のキーに使うためにハッシュ化したりできます。

struct BigInt: ~Copyable, Hashable {
  // ...
  static func == (lhs: borrowing BigInt, rhs: borrowing BigInt) -> Bool { /* ... */ }
  func hash(into hasher: inout Hasher) { /* ... */ }
}

OptionalResult の条件付き適合

Optional / ResultEquatable / Hashable 適合は、中身が non-copyable / non-escapable でも成立するように拡張されます。

extension Optional: Equatable
  where Wrapped: Equatable & ~Copyable & ~Escapable
{
  public static func ==(
    lhs: borrowing Wrapped?, rhs: borrowing Wrapped?
  ) -> Bool
}

extension Optional: Hashable where Wrapped: Hashable & ~Copyable & ~Escapable {
  func hash(into hasher: inout Hasher)
  var hashValue: Int
}

extension Result: Equatable
  where Success: Equatable & ~Copyable, Failure: Equatable
{
  public static func ==(lhs: borrowing Self, rhs: borrowing Self) -> Bool
}

extension Result: Hashable
  where Success: Hashable & ~Copyable & ~Escapable, Failure: Hashable { }

non-copyable な値を包んだ Optional / Result 同士を、従来と同じ感覚で比較したりキーに使えたりするようになります。

文字列化系プロトコル

CustomStringConvertible / CustomDebugStringConvertible / TextOutputStream / TextOutputStreamable~Copyable, ~Escapable に一般化され、non-copyable / non-escapable な型が descriptiondebugDescriptionwrite(to:) などを提供できるようになります。

String(describing:) と文字列補間(DefaultStringInterpolation)も、これに合わせて non-copyable / non-escapable な引数を受け取れるよう拡張されます。

extension String {
  public init<
    Subject: CustomStringConvertible & ~Copyable & ~Escapable
  >(describing instance: borrowing Subject)

  public init<
    Subject: TextOutputStreamable & ~Copyable & ~Escapable
  >(describing instance: borrowing Subject)
}

extension DefaultStringInterpolation {
  mutating func appendInterpolation<T>(
    _ value: borrowing T
  ) where T: TextOutputStreamable & ~Copyable & ~Escapable

  mutating func appendInterpolation<T>(
    _ value: borrowing T
  ) where T: CustomStringConvertible & ~Copyable & ~Escapable
}

これで non-copyable な型も "\(value)" の形で文字列補間に埋め込めます。ただし print のような高次の出力経路は対象外で、non-copyable な Optional を含めてさらに多くの型を Custom*StringConvertible にするには print 側の追加対応が必要なため、今回はスコープ外です。

LosslessStringConvertible だけ ~Escapable を含めない

LosslessStringConvertible~Copyable のみへの一般化で、~Escapable は付きません。これは init?(_ description: String) が値を「作って返す」プロトコルであり、non-escapable な値を返すにはライフタイムの表現が必要になるためです。ライフタイム関連の機能がまだ整っていない段階では ~Escapable には広げず、~Copyable だけ認めることで、参照カウントに頼らない任意精度数値型などを文字列からパースして返す用途に対応します。

protocol LosslessStringConvertible: CustomStringConvertible, ~Copyable { }

Future Directions(参考)

今回のスコープ外として、次のような拡張が speculative に挙げられています(実現を約束するものではありません)。

  • associated type を持つプロトコル(RangeExpression など)を ~Copyable / ~Escapable に一般化する方向性。associated type の Copyable 抑制がまだ言語レベルで整っていないため、整い次第フォローオンの提案で対応される見込みです。
  • Codable / Decodable の non-copyable 対応。これらは associated type こそないものの、実装が重くジェネリックで、non-copyable への一般化が容易ではないため見送られています。
  • InlineArraySpan の条件付き Hashable 適合など、今回の一般化の上に乗る具体的な適合追加。特に SpanEquatable 意味論については別途議論が必要とされています。
  • Optional のような「包み型」が non-copyable / non-escapable な中身を含んだまま Custom*StringConvertible になること。ここは print 側のインフラ改修が必要なため、今回のスコープには含まれていません。