[Elm] より仕様にあった型定義をしてみる（CustumType, OpaqueTypeの応用）

前回勉強してQiitaに投稿したCustomTypeとOpaqueTypeが実開発で活きたので、今回はその話を書きます！



## 題材となるものの仕様

図のような、年代別の出身国の人数をまとめたテーブルを表示するとする。
![](https://i.imgur.com/SHAcohN.png)

表を構成するデータは、以下の形でサーバから取得される。データの集計は全てサーバが行っており、フロント(Elm)はサーバから集計結果を受け取り表示するのみとする。

```json
datas: [
    { age: "10代", country: "アメリカ", people_count:  6 },
    { age: "10代", country: "ブラジル", people_count: 13 },
    { age: "10代", country: "フランス", people_count:  7 },
    { age: "10代", country: 　　　　"", people_count:  2 },
    { age: "10代",              　　　　people_count: 28 },
    { age: "20代", country: "オランダ", people_count: 10 },
    { age: "20代", country: "　　日本", people_count:  4 },
    { age: "20代", country: 　　　　"", people_count:  1 },
    { age: "20代",              　　　　people_count: 25 },
]
```

### countryについて

サーバから受け取った生のデータは

- `country`というfield自体がない（その年代の「全体」を表すデータ）
- `country`が空文字（出身国が「未設定」であるデータ）
- `country`が国名を表す文字列

となっている。

elm-decode-pipelineのoptionalを使いデコード後に

- `"全体"`(optionalのdefaultValueを利用)
- `""`(空文字)
- `"アメリカ"`, `"ブラジル"`...(任意の国名)

というように全てStringに変換する。

さらに空文字の場合は、「未設定」と表示するため変換する。

```elm
if country == "" then
    "未設定"
else
    country
```



## リファクタ前のコードと問題点

`country`の値は全てStringなので、最初Stringで定義してた。

```elm
type Data
    = Data
        { age : String
        , country : String
        , peopleCount : Int
        }
```

### 問題点１

- 全部Stringだけど、「全体」「未設定」「任意の国名」の3パターンに分類できることに気づいた。
- Stringという型定義は仕様に対してちょっと緩く、最適とは言えない。

=> これは**CustumType**の出番！？

### 問題点２

- いろんなところで`country == ""` や `country == "全体"` といった比較表現が出てきてコードが煩雑になってきた。
- countryの内部実装を考えなくても、countryを呼び出したら、「全体」「未設定」「任意の部署名」のどれかのStringが返ってくるようになってたら、使いやすくていいんだけどなぁ。

=> これは**OpaqueType**の出番！？



## リファクタ後のコード

#### Country.elm

```elm
type Country
    = Country Kind

type Kind
    = Total
    | NotSet
    | HasName String
    
new : String -> Country
new value =
    if value == "全体" then
        Country Total

    else if value == "" then
        Country NotSet

    else
        Country (HasName value)
        
isTotal : Country -> Bool
isTotal (Country kind) =
    kind == Total

isNotSet : Country -> Bool
isNotSet (Country kind) =
    kind == NotSet

name : Country -> String
name (Country kind) =
    case kind of
        HasName name ->
            name

        Total ->
            ""

        NotSet ->
            ""
```

#### View.elm

"全体"と表示するか、"未設定"と表示するか、国名を表示するかはViewの責務なのでViewに以下のような条件分岐を持たせる。

```elm
if Country.isTotal data.country then
    "全体"
else if Country.isNotSet data.country then
    "未設定"
else
    Country.name data.country
```

### 問題解決１

- 「全体」「未設定」「任意の国名」の3パターンがあることをコードで表現でき、より仕様に合った型定義になった。
- 型定義がより厳密になり安全になった。
- コードの読み手がコードから仕様をくみ取りやすくなった。

### 問題解決２

- 特に複雑なことを考えなくても、countryを呼び出せばパターン別に対応する文字列が返ってくるので使いやすくなった。
- Countryは内部実装はCustomTypeで実装されているが、それを知らなくてもまるでStringのように扱えるようになっている。
- isTotal, isNotSetといったAPIをCountryモジュールに持たせることで、data.countryの値を渡すだけで、実際にどんな文字列が入っているかを考えずに比較ができるようになった。

**使う側の例１**

```elm
div [] [text <| Country.toString data.country]
```

**使う側の例２**

```elm
-- data.country == "全体" みたいなのを書かなくてよくなった
if Country.isTotal data.country then
    --
else
    --
```



## 余談

今回は以下のような定義付けをした。

```elm
type Country
    = Country Kind

type Kind
    = Total
    | NotSet
    | HasName String
```

OpaqueTypeは必ず名前を同じにしなきゃいけない訳ではないが、名前を同じにする風習があるらしい。
また、CustumTypeも中の個々の型を公開しない(= `Hoge(..)`の形で公開しない)のであれば、内部の型を隠蔽できているのでOpaqueTypeになっていると言える。
今回の`Country`も

```elm
type Country
    = Total
    | NotSet
    | HasName String
```

と定義して`Country`だけ公開すればOpaqueTypeになるが、定義が抽象的すぎてしまうので、あえて`Kind`という要素を定義した。