Swift Digest
SE-0530 | Swift Evolution

Result の async サポート

Async Result Support

Proposal
SE-0530
Authors
Konrad 'ktoso' Malawski, Matt Massicotte
Review Manager
Doug Gregor
Status
Implemented (Swift 6.4)

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

01 何が問題だったのか

標準ライブラリの Result 型は、エラーを throw する可能性のある処理の結果(成功値もしくは失敗の理由)を値として扱うための型で、特に throwing なコードを別の文脈に持ち越したいときに便利です。中でも Result.init(catching:) は、クロージャを実行してその戻り値を .successthrow されたエラーを .failure に詰める便利なイニシャライザで、throwing 関数の結果を Result 値に変換する用途で広く使われてきました。

let result = Result {
    try syncWork()
}

しかし、このイニシャライザは同期クロージャしか受け取れず、非同期コードに対応する版は存在しません。async な処理の結果をそのまま Result に詰めたいケースでは、自前で同等のイニシャライザを書き起こすほかなく、各コードベースで似たようなコードが繰り返し書かれてしまっていました。これは Result 型の利便性を考えると不釣り合いに不便な状況であり、標準ライブラリ側で async 版を提供することが望まれていました。

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

Result に、async クロージャを受け取る init(catching:) のオーバーロードを追加します。これにより、async な throwing コードの結果をそのまま Result 値として組み立てられるようになります。

let result = await Result {
    try await asyncWork()
}

追加されるイニシャライザは次のとおりです。

extension Result where Success: ~Copyable {
    @_alwaysEmitIntoClient
    public nonisolated(nonsending) init(
        catching body: nonisolated(nonsending) () async throws(Failure) -> Success
    ) async {
        do {
            self = .success(try await body())
        } catch {
            self = .failure(error)
        }
    }
}

設計上のポイントは次の点です。

  • Success: ~Copyable の制約により、non-copyable な成功値も扱えます。
  • throws(Failure) で型付きスロー(typed throws)に対応しており、FailureError でなくとも適切な失敗型として捕捉できます。
  • クロージャと初期化処理の双方が nonisolated(nonsending) で宣言されているため、呼び出し元のアクター上で実行されます。@MainActor 上で呼び出せば、クロージャ本体も @MainActor 上で動作します。
  • @_alwaysEmitIntoClient 属性が付与されており、古い OS バージョンへもバックデプロイできます。

利用側のコードは、これまで自前で書いていた Result.init(catching:) の async 版を、標準ライブラリ提供のものに置き換えるだけで済みます。