Range が Codable に適合してなかったので後付けで適合させてみる話と、Codable のエラーハンドリングについて

# Oops!

![Type 'Hoge' does not confirm to protocol 'Decodable' / 'Encodable'](https://qiita-image-store.s3.amazonaws.com/0/34405/e3de54c7-601b-69d3-a656-486c1fe00ddf.png)

Range ( `0...10` みたいなやつ) をメンバとして持つ `Hoge` 型が、コンパイルに失敗してしまいました。
どうも Swift4の Range[^1] は `Codable` に適合していないようです。
困った、どうしたもんでしょうね。

[^1]: ※ Range と一言で言っても Swift4 では複数の型を指しますが、この記事では本題から逸れるため `CountableClosedRange` ( e.g. `1...2` ) にだけ言及しています。

# そうだ、 Codable に後付けで適合させる extension 書けばいいじゃない！

```swift
extension CountableClosedRange: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rangeDescription = try container.decode(String.self)
        let rangeElements = rangeDescription.components(separatedBy: "...")

        guard
            rangeElements.count == 2
            else {
                throw DecodingError.dataCorruptedError(
                    in: container,
                    debugDescription: "\"\(rangeDescription)\" is invalid closed range expression"
                )
        }
        guard
            let l = rangeElements.first.flatMap({ Int($0) }) as? Bound,
            let u = rangeElements.last.flatMap({ Int($0) }) as? Bound
            else {
                throw DecodingError.dataCorruptedError(
                    in: container,
                    debugDescription: "\"\(rangeDescription)\" is composed by non-int bounds"
                )
        }

        lowerBound = l
        upperBound = u
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode("\(lowerBound)...\(upperBound)")
    }
}
```

## 実行結果

```swift
struct Hoge: Codable {
    let range: CountableClosedRange<Int>
}

do {
    let jsonData = """
    {
        "range": "5...10"
    }
    """.data(using: .utf8)!

    let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
    print(t)
    // Hoge(range: CountableClosedRange(5...10))
}

do {
    let t = Hoge(range: 3...9)
    let data = try! JSONEncoder().encode(t)
    let s = String(data: data, encoding: .utf8)!
    print(s)
    // {"range":"3...9"}
}

do {
    let jsonData = """
    {
        "range": "5..<10"
    }
    """.data(using: .utf8)!

    let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
    // Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(
    //   codingPath: [__lldb_expr_100.Hoge.(CodingKeys in _ABCFAB28040915CEF6AA3347992A1FE1).range],
    //   debugDescription: "\"5..<10\" is invalid closed range expression", underlyingError: nil))
}

do {
    let jsonData = """
    {
        "range": "a...z"
    }
    """.data(using: .utf8)!

    let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
    // Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(
    //   codingPath: [__lldb_expr_102.Hoge.(CodingKeys in _79FE350F6AE58BCB4510A745A8A0AB74).range],
    //   debugDescription: "\"a...z\" is composed by non-int bounds", underlyingError: nil))
}
```

`Hoge` 型が無事にコンパイルを通りました。
しかも、 decode, encode, さらにはJSONで想定外な range 表現が現れたときの例外処理も適切に行われています。


# Codable とエラーハンドリング

今回のコードで注目してほしいのは、以下の部分です。

```swift
        guard
            rangeElements.count == 2
            else {
                throw DecodingError.dataCorruptedError(
                    in: container,
                    debugDescription: "\"\(rangeDescription)\" is invalid closed range expression"
                )
        }
        guard
            let l = rangeElements.first.flatMap({ Int($0) }) as? Bound,
            let u = rangeElements.last.flatMap({ Int($0) }) as? Bound
            else {
                throw DecodingError.dataCorruptedError(
                    in: container,
                    debugDescription: "\"\(rangeDescription)\" is composed by non-int bounds"
                )
        }
```

`as? Bound` によって、具体的な `Bound` を知らなくても `init(from decoder: Decoder)` を書くことができました。[^2]
しかも異常な rangeの表現が与えられたときは `guard ... else { throw ... }` で具体的に失敗要因を知らせて失敗させることもできました。
このように `throws` を活用した強力なエラーハンドリングとの統合が、 Codable プロトコルの強みです。

[^2]: `CountableClosedRange` の `Bound` は `SignedInteger` しかありえないと分かっているからこそできる芸当ですが……。

>If any of those look wrong, then the JSON decoder can throw an error and stop the decode right there. After that, we want to convert from things like JSON arrays, and dictionaries, and strings into your types, commits and authors.
That is, after all, the entire point of this API. But there may be more that we can do, and we call that domain-specific validation. (...) These kinds of things can be difficult to express in Swift's type system, but we do think we have a great tool for handling those, and that's just simply writing more Swift code.

[What's New in Foundation - WWDC 2017 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2017/212/?time=2486)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/34405/43009903-4b0b-517f-e4b5-6628041b6528.png)

そういうわけで、 Codable ってやっぱりよくできてるなあというお話でした。

# おまけ: こういうextensionはダメでした

今回、 `Bound` がたまたま `Int` 以外ありえない例だったらからよかったものの、本当なら `as? Bound` でダウンキャストさせるんじゃなくて、 `Bound` が `Int` の場合に限定したコードを書きたいものですよね。
じゃあ、こうしてみましょうか。

```swift
extension CountableClosedRange<Int>: Codable {
    ...
}
```

![Constrained extension must be declared on the unspecialized generic type 'CountableClosedRange' with constraints specified by a 'where' clause](https://qiita-image-store.s3.amazonaws.com/0/34405/69e1b204-9efd-4955-b837-f5af8f650406.png)


おっと。怒られました。 OK、 where で制約を書けばいいんだね？
……と思いきや、

```swift
extension CountableClosedRange: Codable where Bound == Int {
    ...
}
```

![Extension of type 'CountableClosedRange' with constraints cannot have an inheritance clause](https://qiita-image-store.s3.amazonaws.com/0/34405/997b1ab5-73a5-79bc-bf6e-9def132b5875.png)

やっぱりダメでした。
Swift4.0 では、制約条件がついている場合、プロトコルへの適合は不可能なのです。

なお、この問題については Swift Evolution-0143 **Conditional conformances** で解決される予定です。

[swift-evolution/0143-conditional-conformances.md · apple/swift-evolution](https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md)

現在 Accepted 状態です。 Swift4 に入らなかったのは残念……。実装が待ち遠しいですね。


---