エラーを蓄積できる“ResultNel”と数種類のViewから1つを返す型“CoproductView”を作る

# はじめに

Swift 5からは`Result`という他のプログラム言語では主に`Either`としても知られている型が入った。しかしiOSプログラミングをはじめてみると複数のエラーを同時に集めてきてUIに表示するという必要を感じるようになった。そこで`NonEmptyList`というデータ構造を使ってエラーを複数蓄積できるような`Result`である`ResultNel`を開発した。この記事の前半ではまずこの`ResultNel`について解説する。
またiOSプログラミングではUIの作成が必要になる。UI用のフレームワークである[SwiftUI](https://developer.apple.com/documentation/swiftui)を利用して画面を作る場合、`View`というプロトコルを適合した型の値がたとえばテキストや画像といった1つのUIパーツを表現しており、これを束ねて画面全体を構成していくことになる。SwiftUIはSwift 5.1から入った_Opaque Result Type_を使って返り値の型を「なんらかの`View`」と表現することが多いが、これは少々自由すぎる。たとえば「`Text`か`Image`のいずれか1つである」といった制約を与えることができないので`Spacer`といった別のViewを返すことも可能なインターフェースとなってしまう。この記事では`Coproduct`とよばれる特殊なデータ構造を利用して「`Text`か`Image`のいずれか1つを返す」という制約を型レベルにエンコードするため`CoproductView`というデータ構造を自作したためそれを解説する。
なお、この`ResultNel`と`CoproductView`を利用したコードの全体（iOSアプリケーション）は次のGitHubリポジトリーで公開されている。

- https://github.com/y-yu/coffeebrew46

この記事を疑問や改善すべきところなどがある場合には、気軽にコメントやTwitterなどで教えてほしい。

# `ResultNel`: エラーを蓄積できる`Result`

ここでは上記のGitHubリポジトリーのコードと同様に、コーヒーを淹れるためのアプリケーションを開発することを考える[^no_coffee_knowledges]。いま次のようなユーザーの入力をバリデーションして、そのあと入力によりあれこれ計算するようなインターフェースとその実装を考える[^left_success]。

[^no_coffee_knowledges]: あくまでも例なので、コーヒーは飲み物くらいの情報さえ知っていればコーヒーをどうやって作るかの知識はほとんど必要ない。

[^left_success]: ScalaやHaskellなどの`Eitehr`における慣習と異なり、Swiftの`Result`は左側の型パラメーターが成功の型を表す。

```swift:CalculateBoiledWaterAmountService.swift
protocol CalculateBoiledWaterAmountService {
    func calculate(
        coffeeBeansWeight: Double,
        firstBoiledWaterAmount: Double,
        numberOf6: Int
    ) -> ResultNel<BoiledWaterAmount, CoffeeError>
}
```

`ResultNel`はちょっと特殊な`Result`型であり、次のように定義されている。

```swift:ResultNel.swift
public typealias ResultNel<Success, Failure: Error> =
    Result<Success, NonEmptyList<Failure>>
```

## `NonEmptyList`: サイズ1以上を強制するリスト

成功かエラーかのいずれかを表現する`Result`型の失敗側に`NonEmptyList`という「絶対にサイズが1以上のリスト」という型を使ってエラーを束ねている。`NonEmptyList`は普通の`List`（ここでは`ImmutableList`という名前）[^list_naming]を使って次のようになっている。

[^list_naming]: `List`という名前にしたかったが、SwiftUIが提供する`List`と重複するので名前を`ImmutableList`とした。

```swift:ImmutableList
public enum ImmutableList<A> {
    case Nil
    indirect case Cons(A, ImmutableList<A>)
    
    func append(_ l: ImmutableList<A>) -> ImmutableList<A> {
        switch self {
        case .Cons(let h, let t):
            return .Cons(h, t.append(l))
        case .Nil:
            return l
        }
    }
}
```

```swift:NonEmptyList.swift
public struct NonEmptyList<A> {
    let head: A
    let tail: ImmutableList<A>
}

extension NonEmptyList {
    init(_ a: A) {
        self.head = a
        self.tail = .Nil
    }
}
```

`NonEmptyList`は`(A, ImmutableList<A>)`というタプルに特別な名前を付けたものである。普通のサイズが0（`Nil`）となりえる`ImmutableList`を使って`Result<A, ImmutableList<Failure>>`としてしまうと、`Result.failure(.Nil)`というような「`Result`のレベルではエラーだが、エラー値がない！」というハンドルしにくい状態が生じる可能性を型レベルで許可してしまう。このような状態は誰も望んでいないので`NonEmptyList`という長さが1以上（`Nil`がない）のリストをあえて作り、それを`Result`の型パラメーター`Failure`側に入れたものが`ResultNel`[^nel]である。
さて、あとで使うので`NonEmptyList`の結合（append）も作っておく。

[^nel]: `ResultNel`の_Nel_とは“Non Empty List”の頭文字である。

```swift:Associativity.swift
precedencegroup AssociativityLeft {
    associativity: left
}

```

```swift:NonEmptyList.swift
infix operator ++: AssociativityLeft

func ++<A>(_ nel1: NonEmptyList<A>, _ nel2: NonEmptyList<A>) -> NonEmptyList<A> {
    return NonEmptyList(
        head: nel1.head,
        tail: nel1.tail.append(.Cons(nel2.head, nel2.tail))
    )
}
```

## `ResultNel`を連結する演算子`|+|`

さてこれでエラーをいくつも`Failure`側に入れておくことができるようになったので、ついでに次のようなユーティリティー演算子を作っておく[^op_naming]。

[^op_naming]: [Scalaz](https://github.com/scalaz/scalaz)の`Validation`にあわせて、本当は`|@|`という演算子にしたかった。おそらく`@`は_applicative_の`a`から来ているのではないかと思っている。

```swift:Associativity.swift
precedencegroup AssociativityRight {
    associativity: right
}
```

```swift:ResultNel.swift
infix operator |+|: AssociativityRight

public func |+|<A, B, E: Error>(
    r1: ResultNel<A, E>,
    r2: ResultNel<B, E>
) -> ResultNel<(A, B), E> {
    switch (r1, r2) {
    case (.success(let a), .success(let b)):
        return .success((a, b))
    case (.success(_), .failure(let eb)):
        return .failure(eb)
    case (.failure(let ea), .success(_)):
        return .failure(ea)
    case (.failure(let ea), .failure(let eb)):
        return .failure(ea ++ eb)
    }
}
```

こうすることで、`|+|`を使って2つの`ResultNel`をまとめて1つにできる。それを繰り返していけば何個の`ResultNel`があっても1つにまとめることができる。

```swift:ValidateInputService.swift
let validatedTuple: ResultNel<(Double, (Double, Int)), CoffeeError> =
    validateCoffeeBeansWeight(coffeeBeansWeight) |+|
    validateFirstBoiledWaterAmount(
        wholeBoiledWaterAmount: wholeBoiledWaterAmount,
        firstBoiledWaterAmount: firstBoiledWaterAmount
    ) |+|
    validationNumberOf6(numberOf6)

// ..............

private func validateCoffeeBeansWeight(
    _ coffeeBeansWeight: Double
) -> ResultNel<Double, CoffeeError>

private func validateFirstBoiledWaterAmount(
    wholeBoiledWaterAmount: Double,
    firstBoiledWaterAmount: Double
) -> ResultNel<Double, CoffeeError>

private func validationNumberOf6(
    _ numberOf6: Int
) -> ResultNel<Int, CoffeeError>
```

このように個別に1つ1つのバリデーションをつくって、それを適当に`|+|`で繋ぐだけで簡単に全てのバリデーション結果を結合することができる。あとは`Result.map`か何かを使ってバリューオブジェクトなどプログラムで使いやすい型にしてやればよい。

# `CoproductView`: 狙った種類の`View`から1つを返す

最初に書いたソースコードをもう一度下記に貼る。

```swift:CalculateBoiledWaterAmountService.swift
protocol CalculateBoiledWaterAmountService {
    func calculate(
        coffeeBeansWeight: Double,
        firstBoiledWaterAmount: Double,
        numberOf6: Int
    ) -> ResultNel<BoiledWaterAmount, CoffeeError>
}
```

これで`CalculateBoiledWaterAmountService`を実装したクラスがこれらの引数を緻密に検査して、成功すれば`BoiledWaterAmount`型の値を、もし問題があれば1以上の任意の数の`CoffeeError`型のエラーを返すようになった。次の関心は、これをどうやってiOSのUIへ変換するかである。SwiftUIでは画面上のテキストやスペースなどを表すデータ型は`View`に適合（conform）している。だが`View`はプロトコルであるためそのまま返り値の型に書けない。よって次のようなコードはコンパイルが通らない。

```swift
func show(result: ResultNel<BoiledWaterAmount, CoffeeError>) -> View {
    switch result {
    case .success(_):
        return Text("success!")
    case .failure(_):
        return Image("wood-nata")
    }
}
```

返り値の`View`をOpaque Result Typeで`some View`としても、Opaque Result Typeはコンパイル時に型が決定しなければならないため、上記のように`switch`でランタイムの挙動によって返り値が変わるというコードに使うことはできない。このような場合、`AnyView`でラップするという手が使える。

```swift
func show(result: ResultNel<BoiledWaterAmount, CoffeeError>) -> AnyView {
    switch result {
    case .success(_):
        return AnyView(Text("success!"))
    case .failure(_):
        return AnyView(Image("wood-nata"))
    }
}
```

このようにすれば型は通るが、関数`show`は`Text`か`Image`という2種類の`View`のいずれかしか返さないという情報は型にエンコードされない。あえてエンコードしないことで`show`の柔軟さは増すが、場合によっては特定の`View`しか返してほしくないという場合もあるだろう。そこで今回は`CoproductView`を利用してこのような制約を型として与える。

## `Coproduct`とは？

これから紹介する`CoproductView`に利用されている`Coproduct`というデータ構造は、ある意味で`HList`と**逆**のデータ構造である。`HList`は型レベルで要素の型とその位置を保存しており、たとえば次のような`HList`の値があるとする。

```swift
let hlist: HCons<Int, HCons<String, HCons<Double, HNil>>>
```

このとき変数`hlist`は`Int`, `String`, `Double`の型を**全て**持っているような値である。つまりタプルに近い構造で、タプル`(Int, String, Double)`はこの3つの型の値を全て必要とすることと似ている。一方で`Coproduct`でも同じように型レベルリストを作るが、`Coproduct`はその中の**どれか1つ**の値を持っていればよい。つまり、`HList`がリストやタプルの拡張だとすると、`Coproduct`は`Either`や`Result`を拡張したような概念だと考えていいと思う。ではこのような`Coproduct`を作れれば、`Text`や`Image`のうち「どれか1つ」を強制するインターフェースを書くこともできるはずである。

## `CoproductView`の実装

さっそく`CoproductView`を定義していく。

```swift:CoproductView.swift
protocol CoproductView: View {}
```

まずは`CoproductView`はどれかしら1つの`View`を返すことができるので`View`である。そして`HList`の`HCons`と`HNil`に対応する`CCons`と`CNil`を次のように定義する。

```swift:CoproductView.swift
struct CNil<E: View>: CoproductView {
    var body: E
    
    init (_ body: E) {
        self.body = body
    }
}

enum CCons<H: View, T: CoproductView>: CoproductView {
    struct Inl: CoproductView {
        let head: H
        
        init(_ head: H) {
            self.head = head
        }
    }
    
    struct Inr: CoproductView {
        let tail: T
        
        init(_ tail: T) {
            self.tail = tail
        }
    }
    
    case inl(Inl)
    case inr(Inr)
}
```

`CNil`は、`HNil`と違って`View`を適合する型`E`の値を取っている。これは少なくともどれか1つを保証するためであり`CNil`が引数を取らないと0のパターンを許可してしまうためこうしている。
そして厄介なのは`CCons`である。こちらはいろいろな型が含まれているが、`inl`と`inr`のいずれかというEnumであり、それぞれ内部で定義された構造体`Inl`, `Inr`の値を取る。`CCons`の詳しい説明の前の直観として、たとえば次のような`CoproductView`に適合する型の値があるとする。

```swift
let coprouctView: CCons<Text, CCons<Spacer, CNil<Image>>> = ???
```

この例において`coprouctView`は「`Text`か`Spacer`か`Image`のどれか1つ」を表している。つまり（1）`Text`のとき、（2）`Spacer`のとき、（3）`Image`のときの表現が`???`に入りうるが、それが次のようなイメージになる[^image_not_real]。

1. `Text`のとき

    ```swift
    CCons<Text, CCons<Spacer, CNil<Image>>>.Inl( Text("a") )
    ```
2. `Spacer`のとき

    ```swift
    CCons<Text, CCons<Spacer, CNil<Image>>>.Inr(
        CCons<Spacer, CNil<Image>>.Inl( Spacer) )
    )
    ```
3. `Image`のとき

    ```swift
    CCons<Text, CCons<Spacer, CNil<Image>>>.Inr(
        CCons<Spacer, CNil<Image>>.Inr(
            CNil<Image>(Image("wood-nata"))
        )
    )
    ```

[^image_not_real]: これは実際には動作しない。`Inl`や`Inr`のようなEnum内部の構造体のコンストラクターと、`CCons`のコンスタクターである`.inl`や`.inr`をそれぞれ別に呼ばなければ動作はしない。ただ、ただでさえ複雑な型記述がよりややこしくなるし、かつ`.inl`は`Inl`の値しか取らないし`.inr`も同様に`Inr`の値しか取らず、これらを省略してもイメージとしては十分だと考えて簡単のためにこうした。

つまり、いま`CCons<Text, CCons<Spacer, CNil<Image>>>`があったとき次のようになる。

- `Inl`ならば左側の型パラメーターの値となるため、`Text`である
- 一方で`Inr`なら右側の型パラメーターである`CCons<Spacer, CNil<Image>>`となる

このように`CCons`で与えられた2つ型の左または右に入っている（**In L**eft _or_ **In R**ight)ことを表現するのが`Inl`, `Inr`である。
ひとまずこれで「どれか1つ」を型レベルのエンコーディングとして与えることができたが、プロトコル`CoproductView`は`View`であることを要請されている。`View`は次のようなプロトコルである。

```swift:SwiftUI
public protocol View {
    associatedtype Body : View
    var body: Self.Body { get }
}
```

あとはどうにか`CCons.Inl`, `CCons.Inr`などを`View`に適合させていけばいい。

### `CCons.Inl`を`View`に適合させる

こちらは比較的やりやすい。なぜなら`CCons.Inl`は次のように`View`型に適合した型`H`の値`head`をもっている。

```swift
struct Inl: CoproductView {
    let head: H
}
```

したがって次のようなエクステンションを書けばいい。

```swift:CoproductView.swift
extension CCons.Inl: View {
    typealias Body = H
    
    var body: H {
        get {
            return head
        }
    }
}
```

### `CCons.Inr`を`View`に適合させる

`Inr`は少しややこしいが、`CCons`は次のようになるはずである。

- 型パラメーター`T`には`CoproductView`として型レベルリストがはいっているが、`Inl`があればそれが`View`に適合する値を持っている
- `CoproductView`の型レベルリストの末尾となりうる`CNil`は、必ず`View`に適合する値を持っている
    - `CNil`自体も`View`に適合しているため、`CNil`の値は常に`body`を呼ぶことができる

したがって次のように型`T`の値である`tail`の`body`を呼びだしてそれをそのまま返せばよい。

```swift:CoproductView.swift
extension CCons.Inr: View {
    typealias Body = T.Body
    
    var body: T.Body {
        get {
            return tail.body
        }
    }
}
```

### `CCons`を`View`に適合させる

最後に`CCons`そのものを`View`に適合させる。

```swift:CoproductView.swift
extension CCons: View {
    var body: AnyView {
        switch self {
        case .inl(let left):
            return AnyView(left.body)
        case .inr(let right):
            return AnyView(right.body)
        }
    }
}
```

見てわかるように、ここでは`AnyView`を使ってしまっている。`.inl`と`.inr`のケースでそれぞれ帰ってくる`View`の型が違うためこうする他ない。`CCons`を`View`として使うのはアプリケーションのMVVMでいうところのViewになり、プログラマーがここを明示的に呼ぶコードを書く必要はないため、ここの型が分からなくなることで大きな問題はないと考えている。
最後に`.inl`とか`.inr`とかを手で呼ぶのを少しでも楽にするためのコンスタクターを次のように作ってようやく終了である。

```swift:CoproductView.swift
extension CCons {
    static func apply<H: View, T: CoproductView>(_ head: H) -> CCons<H, T> {
        return CCons<H, T>.inl(CCons<H, T>.Inl(head))
    }
    
    static func apply<H: View, T: CoproductView>(_ tail: T) -> CCons<H, T> {
        return CCons<H, T>.inr(CCons<H, T>.Inr(tail))
    }
}
```

## `CoproductView`を使ったインターフェース

それでは最初に紹介した`CalculateBoiledWaterAmountService`の結果を使って`CoproductView`をつくるような`BoiledWaterAmountPresenter`を作っていく。まずはプロトコルでこのようなインターフェースを与える。

```swift:BoiledWaterAmountPresenter.swift
protocol BoiledWaterAmountPresenter {
    associatedtype ResultView: CoproductView

    func show(result: ResultNel<BoiledWaterAmount, CoffeeError>) -> ResultView
}
```

そして、これを次のように実装する。

```swift:BoiledWaterAmountPresenter.swift
class BoiledWaterAmountPresenterImpl: BoiledWaterAmountPresenter {
    typealias ResultView = CCons<Text, CNil<Image>>
    
    func show(result: ResultNel<BoiledWaterAmount, CoffeeError>) -> CCons<Text,
    CNil<Image>> {
        switch result {
        case .success(let boiledWaterAmount):
            return CCons<Text, CNil<Image>>
                .apply(Text("Boiled water amounts are " + boiledWaterAmount.toString()))
        case .failure(_):
            return CCons<Text, CNil<Image>>
                .apply(CNil(Image("wood_nata")))
        }
    }
}
```

このように`.success`のときは`Text`を、そしてエラーのときは`Image`を返している。これをエミュレーターで実行すると次のようになる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10815/62c455d4-3c7c-14eb-7556-feb0fcff8609.gif" width="40%" align="center"/>

セグメンテーションフォールトなども（少なくともいまのところは）起きていない。

# まとめ

`ResultNel`で複数のエラーを同時にハンドルしつつ、`CoproductView`で`Text`や`Spacer`など狙った型だけを生成する関数を型で制約しつつ作れるようになった。`Text`などUIコンポーネントは生成にコストがかかるらしいので、あらかじめ生成しておいてそれを使い回すなど工夫は必要かもしれない。そもそもUIを作るうえでは、エラーメッセージは特別な場所に出すといった特定の範囲のコンポーネントだけを操作しても実は意味がないのかもしれない。とはいえ、SwiftではじめてUIを作っていろいろなことが分かって楽しかった。個人的には`CoproductView`の方がおもしろいと思うが、`ResultNel`の方が実用的なのかもしれない。

# 謝辞

このアプリケーションの開発は[@7_6_](https://twitter.com/7_6_)さんにたくさんの助言を頂いた。MVVMすらよく知らなかった（？）筆者をいろいろサポートしてくれたことなど、たくさんの貢献に感謝したい。