【Swift】Enumのassociated Valueがあるときのパターンマッチで単純にif文使ったらエラー出た

Enumのassociated Value、便利なことに気づいたのでよく使うようになったんですが、
この前ちょっと不便だなと思うことがあったので、ストーリー形式で書いてみます。

# 〜associated Value使う前〜
Twitterライクなアプリの開発に入ってたとして、下記みたいなStructがありました。

```swift
import Foundation

struct Followee {
    
    enum Status {
        case follow
        case blocked
        case mute
    }
    
    var status: Status = .follow // ホントは別のところでセットするけどわかりやすく
    
    var canAppearTimeline: Bool {
        if status == .follow {
            return true
        } else {
            return false
        }
    }
}
```

フォローしてるアカウントの投稿をタイムラインに表示するかどうかを、
`canAppearTimeline`で判定しています。
現状、フォローしてるアカウントのステータスは、フォロー中・被ブロック・ミュート中の三つで、フォロー中のときだけ表示します。
シンプルですね。

# 「タイムラインで表示する優先順位づけできひん？」
ここで仕様変更が入って、

「タイムラインで表示する優先順位づけできひん？」

という要望が入りました。

「なんかミュートまでは行かへんけど、この人タイムラインに出過ぎてうっとうしいわ〜ってときあるやろ？」
「嫌いまではいかへんけど、毎日食べると胃もたれする料理みたいな」（？）
「そういうとき、表示頻度を下げる機能あったら、嬉しいよな」

というわけで、仕様変更することになりました。

# Statusを追加
とりあえずStatusをひとつ追加することにしました。

```swift
    enum Status {
        case follow
        case blocked
        case mute
        case showLess(priority: Int) // New!
    }
```

# ロジック
こう考えました。

「うーん、めんどくさいな……どないしよ……」
「もうええわ。優先順位の初期値１入れて、タイムラインにあらわれるたびに１ずつ増やしてって、１０になったら表示にしたろ！」
「とりあえず表示頻度減ったらええやろ」

機能としてはどうかと思いますが、ロジックは単純になりそうですね。

# ところが
ところが、Statusを変更した段階で、`Protocol 'Equatable' requires that 'Followee.Status' conform to 'Equatable'`というエラーが出るようになりました。

```swift
    var canAppearTimeline: Bool {
        if status == .follow {
            return true
        } else {
            return false
        }
    }
```
「な、何もしてないのにif文が通らなくなった！」

# associated Valueがあるとなぜifのパターンマッチができないのか
「Equatableに従ってないと怒られるんなら、従わせればええんか……？」
と思って、Statusをこうしてみました。

```swift
    enum Status: Equatable {
        case follow
        case blocked
        case mute
        case showLess(priority: Int)
    }
```

これをやると、showLess以外のケースであればコンパイル通るようになります。
しかし……

```swift
    var canAppearTimeline: Bool {
        if status == .showLess(_) { // Missing argument label 'priority:' in call
            // showLessのときのロジックを書く
        }
    }
```

こんな書き方はできません。

```swift
    var canAppearTimeline: Bool {
        if status == .showLess(priority: 1) {
            // showLessのときのロジックを書く
        }
    }
```
これなら通ります。

「えっ、これ１から１０まで書かないとアカンの？！」

```swift
    var canAppearTimeline: Bool {
        if status == .showLess(priority: 1) || status == .showLess(priority: 2) ||
           status == .showLess(priority: 3) || status == .showLess(priority: 4) ||
           status == .showLess(priority: 5) || status == .showLess(priority: 6) ||
           status == .showLess(priority: 7) || status == .showLess(priority: 8) ||
           status == .showLess(priority: 9) || status == .showLess(priority: 10) {
            // showLessのときのロジックを書く
        }
    }
```

「こうやな！」

これなら通りますが、

「すまん！やっぱpriorityもっと細かくしてくれ！１〜１００にしよう！」

と言われた瞬間に「アアアアアアアアアアアアアアア」となってしまいますね。

そう。Associated Valueは値を保存してくれるので、マジメにパターン分けすると、
Associated Valueのとりうる値分のパターンがあるんですね〜

# 対処法
対処法は二つあります。
まずswitch文を使う方法。

```swift
    var canAppearTimeline: Bool {
        switch status {
        case .follow:
            // 処理
        case .blocked:
            // 処理
        case .mute:
            // 処理
        case .showLess(priority: let priority):
            // 処理
        }
    }
```

これはEnum使うときのパターンマッチの王道ですね。

ただ状況によっては、もっとif文ぽく書きたいときもあると思います。
たとえばEnumが10ケースあるけど、ある1ケースに該当するときだけ処理を変えて、あとの9パターンは何もしない、みたいなときとか。
そんなときにSwitch文書くのはちょっと大袈裟ですよね。

そんなときのために`if case`という構文があります。

```swift
        if case .showLess(let priority) = status {
            if priority < maxPriority {
                increment()
                return false
            } else {
                reset()
                return true
            }
        }
```
こんな感じで書けます。
`case .xxx`のあとに=でつないで、Enumが来るっていうちょっと気持ち悪い構文ですが、
まあ元々Switch文を書かなきゃいけないところの省略形だと思えば多少は……いやそうでもないですか。

# 完成形
```swift
import Foundation

struct Followee {
    enum Status {
        case follow
        case blocked
        case mute
        case showLess(priority: Int)
        
        mutating func countUp() {
            if case .showLess(let priority) = self { self = .showLess(priority: priority + 1) }
        }
        
        mutating func resetPriority() {
            if case .showLess(_) = self { self = .showLess(priority: 1) }
        }
    }
    
    var status: Status = .follow
    let maxPriority = 10
    
    var canAppearTimeline: Bool {
        if case .follow = status {
            return true
        } else if case .showLess(let priority) = status {
            if priority < maxPriority {
                increment() // 直接countUp()呼んだら怒られたので迂回している
                return false
            } else {
                reset()
                return true
            }
        }
        return false
    }
}

func increment() {
    followee.status.countUp()
}

func reset() {
    followee.status.resetPriority()
}

var followee = Followee()
followee.status = .showLess(priority: 1)
(1...10).forEach { _ in print(followee.canAppearTimeline)}
print(followee.status)
```

```swift:出力
false
false
false
false
false
false
false
false
false
true
showLess(priority: 1)
```

# 余談
この記事書くために調べてたら[知った](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html#ID148)んですが、

```swift
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
```

これを、

```swift
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
```

こんな書き方もできるんですね。へえ〜って感じです。

あとrawValueとassociated Valueは一緒に使えないとか、便利な分色々制約あるんだな〜と思いました。
Enumの持ってる単純性を損なっちゃうデメリットもありますね。

# 参考
[Enumerations](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html)
[SwiftのEnumをif文で比較できない（Associated Value）](https://qiita.com/y_koh/items/204f04ab11677bd73444)