[Swift] rethrowsを少し整理してみた | Developers.IO

[Swift] rethrowsを少し整理してみた

はじめに

モバイルアプリサービス部の中安です。

Swiftには rethrows という予約語(キーワード)があります。

実はこの rethrows の役割が頭の中で少し整理できていなかったので、 今回は自分で自分に教えるような感覚でブログに書き記したいと思います。

そもそもこれって何だっけ?

rethrows は throws と同じ例外関係のキーワードで、「高階関数」と関連が強いものです。

「高階関数」は、平たく言うと map や sort などのように関数を引数に持たせるような処理のことです(関数型引数ともいう)。
(厳密にはwikipediaを参照)

たとえば Swiftでは、コレクションの map は以下のように定義されています。

public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

初めて Swift に触るくらいの人には「なんのこっちゃ」な定義です。

普通に高階関数を使用すると

let strings = [1, 2, 3, 4, 5, 6].map { n -> String in
    return "\(n)"
}
print(strings)
// ["1", "2", "3", "4", "5", "6"]

整数(Int)の配列を文字列の配列にするだけの処理です。

特に例外の出るような処理はないので、この場合は、 rethrows は関係ありません。

実際に例外が引き起こされると

例えば「4という数字は縁起が悪いので例外」ということにしちゃいましょう。

struct EngiWaruiError: Error {}

let strings = [1, 2, 3, 4, 5, 6].map { n -> String in
    if n == 4 {
        throw EngiWaruiError()
    }
    return "\(n)"
}
print(strings)

例外の EngiWaruiError がスローされることがわかると、 コンパイラが Call can throw but is not marked with 'try' と怒ってきます。

つまり try を書けと。エラーを補足しろと。

では、この場合どこに try を書くのが適切でしょうか。

答えは代入する直前。配列の前になります。 例外処理を入れるために do-try 節も追記して下記の通りになります。

struct EngiWaruiError: Error {}

do {
    let strings =  try [1, 2, 3, 4, 5, 6].map { n -> String in
        if n == 4 {
            throw EngiWaruiError()
        }
        return "\(n)"
    }
    print(strings)
} catch let e {
    print(e)
}

これで例外の補足ができるようになりました。 上記例であれば、4が配列の中にある限り必ず例外がスローされるので、一度4を抜いてみれば正常型の動きをすると思います。

何かがおかしい・・・?

さて、ここまでの書き方は特に違和感なくスッと入ってくるのですが、よく考えてみると変な話です。

throw EngiWaruiError() をしているのは mapメソッドの引数の関数内スコープの話です。

しかし、try をして catch をしているのは、map を呼び出す側でしていることに気づきます。

これが rethrows の恩恵 ということになるわけです。

もう少し追いかけてみる

map メソッドはおそらく内部的には下記のようなことが行われているのではないかと思います。

    func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] {
        var res = [T]()
        for element in self {
            res.append(try transform(element))
        }
        return res
    }

ここから rethrows キーワードだけを抜いてみます。

extension Array {
    
    func mapWithoutRethrows<T>(_ transform: (Element) throws -> T) -> [T] {
        var res = [T]()
        for element in self {
            res.append(try transform(element))
        }
        return res
    }
}

たちまち Errors thrown from here are not handled というコンパイルエラーで怒られてしまいます。

transform という関数型の引数が throws 、 すなわち「例外を起こすかもしれないよ」と定義されているのに、 「try をしても catch をするやつが居ないじゃないか」というお怒りです。

つまり、 rethrows は「例外がスローされるかもしれない引数の関数」内で実際に例外が発生した場合、 「呼び出し元にその例外を伝えるので、関数型引数内で例外をハンドルしなくてもいいよ」というキーワードになります。

これにより、関数型引数内で起きた例外は大元で捉えることができます。

ちなみに

map メソッドから例外関係をすべて抜き去るとどのようになるかも試してみます。

extension Array {
    
    // throwsもrethrowsもない
    func mapWithoutThrowsAndRethrows<T>(_ transform: (Element) -> T) -> [T] {
        var res = [T]()
        for element in self {
            res.append(transform(element))
        }
        return res
    }
}

let strings =  [1, 2, 3, 4, 5, 6].mapWithoutThrowsAndRethrows { n -> String in
    return "\(n)"
}
print(strings)

上記のように関数型引数内で一切例外が起きないのであれば、これでも成立します。

しかし、例外が起きる場合

extension Array {
    
    // throwsもrethrowsもない
    func mapWithoutThrowsAndRethrows<T>(_ transform: (Element) -> T) -> [T] {
        var res = [T]()
        for element in self {
            res.append(transform(element))
        }
        return res
    }
}

struct EngiWaruiError: Error {}

do {
    let strings =  [1, 2, 3, 4, 5, 6].mapWithoutThrowsAndRethrows { n -> String in
        if n == 4 {
            throw EngiWaruiError()
        }
        return "\(n)"
    }
    print(strings)
} catch let e {
    print(e)
}

Type of expression is ambiguous without more context。「型がよくわからないぜ」と怒られます。 例外スローされる関数型なのに「スローされないぜ」という型として宣言している格好になっているからですね。

この場合は、一応、関数内部で do-catch させることで回避はできます。

let strings =  [1, 2, 3, 4, 5, 6].mapWithoutThrowsAndRethrows { n -> String in
    do {
        if n == 4 {
            throw EngiWarui()
        }
        return "\(n)"
    } catch {
        return ""
    }
}
print(strings)
// ["1", "2", "3", "", "5", "6"]

しかし、そもそもこうなってしまうと返したくもない文字列を返さざるをえないことになりますし、 非常に使いづらい状態であるといえます。

やはり、こういう場合は rethrows を活用したほうが良いかと思います。

まとめ

以上の説明で上手く伝わったでしょうか。

Swift の高階関数は優秀なものが多いので、自作する機会というのは少ないと思いますが、 もしも実装することがあれば、関数型引数内での例外について意識して作ってみてください。

なんで rethrows を付けなきゃいけなかったんだっけ? となったら、この記事を思い出していただけると嬉しいです。