Swift Digest

Data のクロージャベース関数の一般化

Generalize closure-based functions of Data

Proposal
SF-0038
Authors
Guillaume Lessard
Status
Awaiting implementation or Awaiting review

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Data には withUnsafeBytes(_:) をはじめとするクロージャベースの API がありますが、これらのシグネチャは Swift の比較的新しい言語機能を取り込めていませんでした。具体的には次の 2 点です。

  • SE-0427 によって non-copyable な型がジェネリクスに参加できるようになり、SE-0437 では UnsafeBufferPointer<T> などの低レベルなプリミティブが一般化されました。これに伴い、withUnsafeBytes() のようなジェネリック関数が non-copyable な値を返せることが期待されるようになっています。
  • SE-0413 で typed throws が導入され、関数が投げるエラー型を型レベルで指定できるようになりました。これは Embedded Swift のような高性能な文脈で重要です。

しかし Data の現状の API は次のように、ResultType が暗黙に Copyable であることを要求し、エラーは rethrows で扱う形にとどまっていました。

extension Data {
    public func withUnsafeBytes<ResultType>(
        _ apply: (UnsafeRawBufferPointer) throws -> ResultType
    ) rethrows -> ResultType
}

このため、クロージャの中で non-copyable な値(たとえば non-copyable な型のインスタンス)を構築して返したり、クロージャが投げるエラー型を呼び出し側で具体的な型として受け取ったりすることができませんでした。

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

Data のクロージャベースの 3 つの関数 withUnsafeBytes(_:) / withContiguousStorageIfAvailable(_:) / withUnsafeMutableBytes(_:) を、typed throws と non-copyable な戻り値の両方を受け取れるように一般化します。

新しいシグネチャは次のとおりです。

extension Data {
    @_alwaysEmitIntoClient
    public func withUnsafeBytes<E: Error, ResultType: ~Copyable>(
        _ apply: (UnsafeRawBufferPointer) throws(E) -> ResultType
    ) throws(E) -> ResultType

    @_alwaysEmitIntoClient
    public func withContiguousStorageIfAvailable<E: Error, ResultType: ~Copyable>(
        _ body: (_ buffer: UnsafeBufferPointer<UInt8>) throws(E) -> ResultType
    ) throws(E) -> ResultType?

    @_alwaysEmitIntoClient
    public mutating func withUnsafeMutableBytes<E: Error, ResultType: ~Copyable>(
        _ body: (UnsafeMutableRawBufferPointer) throws(E) -> ResultType
    ) throws(E) -> ResultType
}

ポイントは次の 2 つです。

  • ResultType の制約が ~Copyable になり、コピー可能な型に加えて non-copyable な型も戻り値として返せるようになります。
  • エラーは rethrows ではなく throws(E) の typed throws になります。E == any Error の場合がこれまでの挙動と一致するため、既存の呼び出しコードはそのまま動きます。

これらは既存の関数を ソース互換 に置き換える新しい関数として導入されます。これまでは ResultTypeCopyable 必須だったところに ~Copyable を許容する形になり、また rethrows が投げていた untyped なエラーは「E == any Error の typed throws」の特殊ケースとして包含されるため、これまでの呼び出しはそのまま型チェックされます。

ABI 安定なプラットフォームでは既存の ABI は維持され、新しいエントリポイントは @_alwaysEmitIntoClient で配布されます。これによりコンパイラが利用箇所ごとに特殊化しやすくなっています。

利用例

たとえば、Data の中身を見て非同期処理用に OutputSpan のような non-copyable な値を組み立てたいケースや、独自のエラー型を typed throws でそのまま伝播させたいケースが書けるようになります。

enum ParseError: Error {
    case tooShort
    case invalidHeader
}

func readHeader(from data: Data) throws(ParseError) -> UInt32 {
    try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws(ParseError) -> UInt32 in
        guard buffer.count >= 4 else { throw ParseError.tooShort }
        let value = buffer.load(as: UInt32.self)
        guard value != 0 else { throw ParseError.invalidHeader }
        return value
    }
}

呼び出し側は tryParseError をそのまま受け取れるので、as? ParseError のようなキャストが不要になります。non-copyable な戻り値を返したいケースでも、同じシグネチャで対応できます。