🗜️

【Swift 5.5】詳解 Result

2021/12/09に公開

この記事は DeNA 21新卒×22新卒内定者 Advent Calendar 2021 #DeNA21x22AdCal の9日目の記事です。

https://qiita.com/advent-calendar/2021/dena-21x22


Result とは Swift 5.0 で導入された、成功または失敗のいずれかを表すことのできる型で、成功失敗それぞれの場合における値を associated values として持たせることもできます。

この記事では Result が持つイニシャライザ、メソッド、プロパティ、演算子について説明します。

この記事で取り扱う Swift のバージョンは 5.5 です。

Result の定義

Result はここで定義されており、実態は enum となっています。
https://github.com/apple/swift/blob/main/stdlib/public/core/Result.swift

@frozen enum Result<Success, Failure> where Failure : Error

成功・失敗のそれぞれの場合に伝えたい値を入れるための Success, Failure と2つのジェネリクスが用意されています。ただし FailureError に準拠している必要があります。

たとえば、成功した場合は String、失敗した場合は MyError を伝えたいときはこのようにします。

enum MyError: Error {
    case invalid
}

let result: Result<String, MyError>

また、ResultEquatableHashableSendable に準拠しています(ただし SuccessFailure がどちらもそれらに準拠していることが条件)。

Result が準拠しているプロトコル
extension Result: Equatable where Success: Equatable, Failure: Equatable { }
extension Result: Hashable where Success: Hashable, Failure: Hashable { }
extension Result: Sendable where Success: Sendable, Failure: Sendable { }

Result の基本

Result は2つの case を持ちます。

.success(_:)

.success(_:) はその Result が成功していることを示したいときに使います。成功した際に伝えたい値を associated value として持ちます。

let result: Result<String, MyError> = .success("成功!")
print(result) // success("成功!")

.failure(_:)

.failure(_:) はその Result が失敗していることを示したいときに使います。失敗した際に伝えたい Error を associated value として持ちます。

enum MyError: Error {
    case invalid
}

let result: Result<String, MyError> = .failure(.invalid)
print(result) // failure(MyError.invalid)

基本の使い方

Result は enum なので、たとえば switch 文を使って簡単に成功・失敗した際の続きの処理を書くことができます。

たとえば do-catch の場合は catch で得られるエラーが Error であるということしか分かりません。しかし ResultFailure に自分で指定した MyError が入ることが確定しています。

let result: Result<String, MyError> = // ...

switch result {
case .success(let str):
    print("成功した:", str)
case .failure(let myError):
    print("失敗した")
    switch myError {
    case .invalid:
        print("invalid")
    }
}

throws なメソッド等を Result に変換する

Result のイニシャライザに init(catching:) があります。これを使うことで do-catch で囲む必要のある throws なメソッド等を Result に変換できます。

func doSomething() throws -> String {
    return "成功"
    // または throw MyError.invalid など
}

let result: Result<String, Error> = .init(catching: doSomething)
print(result) // success("成功")
// async throws なメソッド等でも使えるように自分で拡張する一例
extension Result where Failure == Swift.Error {
    init(catching body: () async throws -> Success) async {
        do {
            self = .success(try await body())
        } catch {
            self = .failure(error)
        }
    }
}

func doSomething() async throws -> String {
    return "成功"
    // または throw MyError.invalid など
}

let result: Result<String, Error> = await .init(catching: doSomething)
print(result) // success("成功")

Result を throws なメソッドに変換する

また逆に Resultget() を使うことによって、Result を do-catch で囲む必要のある throws なメソッドに変換することもできます。

let result: Result<String, Error> = .success("成功!")

do {
    let str = try result.get()
    print(str) // 成功!
} catch {
    print(error)
}
func doSomething() async -> Result<String, MyError> {
    return .success("成功!")
    // または return .failure(.invalid) など
}

do {
    let result = try await doSomething().get()
    print(result)
} catch {
    print(error)
}

map(_:)flatMap(_:)

Result を別な Result に変換する

map(_:)mapError(_:) が用意されており、すでに得た Result を別な Result に変換できます。

map(_:)

let intResult: Result<Int, Error> = .success(20211225)
print(type(of: intResult)) // Result<Int, Error>
print(intResult) // success(20211225)

let stringResult = intResult.map { String($0) }
print(type(of: stringResult)) // Result<String, Error>
print(stringResult) // success("20211225")

mapError(_:)

import Foundation

struct MyError1: Error {
}

struct MyError2: Error {
    let error: Error
    let uuid: UUID
    
    init(_ error: Error) {
        self.error = error
        self.uuid = UUID()
    }
}

let result1: Result<Int, MyError1> = .failure(MyError1())
print(type(of: result1)) // Result<Int, MyError1>
print(result1) // failure(MyError1())

let result2 = result1.mapError { MyError2($0) }
print(type(of: result2)) // Result<Int, MyError2>
print(result2) // failure(MyError2(error: MyError1(), uuid: 423CF494-F5DE-42DC-B8E7-0198BFA041F7))

別な Result に変換するときにネストを回避する

flatMap(_:)flatMapError(_:) が用意されています。map(_:)mapError(_:) のように、すでに得た Result を別な Result に変換できますが、Result がネストされることがなくなります。

下記の result2result3 を見比べてみましょう。

flatMap(_:)

let result1: Result<Int, Error> = .success(20211225)
print(type(of: result1)) // Result<Int, Error>
print(result1) // success(20211225)

let result2 = result1.map {
    Result<Int, Error>.success($0 + 10000)
}
print(type(of: result2)) // Result<Result<Int, Error>, Error>
print(result2) // success(Result<Int, Error>.success(20221225))

let result3 = result1.flatMap {
    Result<Int, Error>.success($0 + 10000)
}
print(type(of: result3)) // Result<Int, Error>
print(result3) // success(20221225)

flatMapError(_:)

import Foundation

struct MyError1: Error {
}

struct MyError2: Error {
    let error: Error
    let uuid: UUID
    
    init(_ error: Error) {
        self.error = error
        self.uuid = UUID()
    }
}

let result1: Result<Int, MyError1> = .failure(MyError1())
print(type(of: result1)) // Result<Int, MyError1>
print(result1) // failure(MyError1())

let result2 = result1.mapError { MyError2($0) }
print(type(of: result2)) // Result<Int, MyError2>
print(result2) // failure(MyError2(error: MyError1(), uuid: B1DCC40D-907F-4075-A7C0-B4A1B47A1005))

let result3 = result1.flatMapError {
    Result<Int, MyError2>.failure(MyError2($0))
}
print(type(of: result3)) // Result<Int, MyError2>
print(result3) // failure(MyError2(error: MyError1(), uuid: E98BDFDB-50BF-46F9-98A5-52343F52BD72))

Combine の Publisher を使う

Result には publisher もあり、 sink(receiveValue:)sink(receiveCompletion:receiveValue:) などを使うことができます。

let result: Result<Int, Error> = .success(20211225)
result.publisher
    .sink { completion in
        print(type(of: completion))
        print("result receiveCompletion:", completion)
    } receiveValue: { value in
        print(type(of: value))
        print("result receiveValue:", value)
    }

// Int
// result receiveValue: 20211225
// Completion<Error>
// result receiveCompletion: finished

もし Result.failure(_:) となった場合、Subscribers.Completion.failure(_:) となります。エラーの内容は Result.failure(_:) から Subscribers.Completion.failure(_:) に渡っています。

enum MyError: Error {
    case invalid
}

let result: Result<Int, MyError> = .failure(.invalid)
result.publisher
    .sink { completion in
        print("result receiveCompletion:", completion)
    } receiveValue: { value in
        print("result receiveValue:", value)
    }

// Completion<MyError>
// result receiveCompletion: failure(MyError.invalid)

お知らせ

この記事は DeNA 21新卒×22新卒内定者 Advent Calendar 2021 #DeNA21x22AdCal の9日目の記事でした。他の記事もぜひどうぞ!

https://qiita.com/advent-calendar/2021/dena-21x22

参考

https://github.com/apple/swift/blob/release/5.5/stdlib/public/core/Result.swift
https://developer.apple.com/documentation/swift/result
https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md

DeNA Engineers

Discussion