【Elm初心者】単純な型の実装方法を比較する

[Elmを完全に理解した](https://twitter.com/cubbit2/lists/elm/members)初心者です。

Elmでは型を定義する方法が複数用意されています。

例として、認証機能を持つシステムのユーザーIDを考えてみます。
実質ただの文字列という __単純な構造だと選択肢が多くて__ 初心者は悩んでしまうでしょう。

今回はこの例について型の実装方法のパターンとその採用基準について考察しました。


## 1. プリミティブ型

まずはユーザーIDを単純に`String`型として扱うことが可能です。

最大のメリットは型として宣言する必要がなくて楽なことです。1つの小規模なモジュールでしか使われない型であればこれで十分です。しかし、ユーザーIDのようにシステムの核となるデータを`String`で扱うのは得策ではありません。

たとえば以下のような型注釈を持つパスワードリセットメール送信関数を考えてみます。

```elm
sendPasswordReset : String -> String -> Cmd msg
sendPasswordReset userId email =
    ...
```

型注釈に2つの`String`型が出てきますが、実装部分を読まないとどの`String`が何を表しているかがわかりません。これでは __可読性が低く、誤った値を関数に適用してバグになる可能性があります__ 。


## 2-1. 型の別名

型注釈をより明確にするには、[型の別名](https://guide.elm-lang.jp/types/type_aliases.html) を使う方法があります。

```elm
type alias UserId
    = String

type alias Email
    = String


sendPasswordReset : UserId -> Email -> Cmd Msg
sendPasswordReset userId email =
    ...
```

`String`型に名前を付けることで __引数に意味をもたせる__ ことができました。

しかしこれだけでは不十分な点があります。別名を付けた型も実態はただの`String`なので、以下のコードは問題なくコンパイルできてしまいます。

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
    let
        notUserId : String
        notUserId = "simple string"
        notEmail : UserId
        notEmail = "abc123"
    in
    ( model
    , sendPasswordReset notUserId notEmail -- コンパイルできるよ
    )
```

型注釈が見やすくなったとはいえ、誤った値を関数に適用できる問題は残ったままです。


## 2-2. 型の別名 (レコード型)

「ただ1つのフィールドを持つレコード型の別名」として実装することもできます。

```elm
type alias UserId =
    { content : String }


type alias Email
    = String


update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
    let
        notUserId : String
        notUserId = "Simple string!"
        email : Email
        email = "mail@mail.com"
    in
    ( model
    , sendPasswordReset notUserId email -- これはコンパイルエラー！
    )
```

`UserId`は`String`と異なる型になるためコンパイルエラーとして検出できるようになります。

しかし、所詮は型の別名に過ぎないことに注意が必要です。
`Email`も同じフィールド名を使って以下のように書いたりすれば`UserId`と同じ型になってしまい、`sendPasswordReset`の第一引数に`Email`を使えてしまいます。

```elm
type alias UserId =
    { content : String }


type alias Email =
    { content : String }


update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
    let
        notUserId : Email
        notUserId = { content = "mail@mail.com" }
        notEmail : UserId
        notEmail = { content = "abc123" }
    in
    ( model
    , sendPasswordReset notUserId notEmail -- コンパイルできるよ
    )
```

ここでは2つの型を並べて書いているので馬鹿みたいですが、別モジュールに書いていると気づかないかもしれません。

さらにこのパターンは、`UserId`から`.content`を使って中身を取り出したり、逆にラップしてやる手間が生じるので記述量が多くなるというデメリットがあります。

```elm
sendPasswordReset : UserId -> Email -> Cmd Msg
sendPasswordReset userId email =
    let
        userIdString = userId.content
    in
    ...
```

次の案と比較してもメリットが中途半端な感じなので、今回のようなフィールドが1つの場合は却下していいでしょう。


## 3-1. カスタム型

カスタム型を使うことで __他の型と取り違えられる問題を解決することができます__ 。

カスタム型にする場合の設計は[こちらの記事](https://qiita.com/arowM/items/54fd76ee3965ddc86455)が参考になります。

フィールドが1個だけならコンストラクタの書き方による記述量や関数の種類の違いは大差ないので、他のメリットを考慮して`opaque type` としてモジュールを作成しましょう。

```elm
module UserId
  exposing
    ( UserId
    , fromString
    , unwrap
    )


type UserId =
    UserId String


fromString : String -> UserId
fromString =
    UserId


unwrap : UserId -> String
unwrap (UserId string) =
    string
```

1つ前のパターンと同じく記述量が多くなるのでやたらと多用すると面倒になりますが、ユーザーIDであれば保守性のメリットが上回ると思います。


## 3-2. カスタム型 (レコード型？)

こんな書き方もできます。3-1と同様のメリットがありますが、記述量がさらに増えるだけなので却下でいいでしょう。

```elm
type UserId
    = UserId { content : String }
```


## 結論

単純な構造の型については以下のような結論になりました。
* 小範囲でしか使わない型はそのままプリミティブ型として扱う
* その中で型注釈を明確にしたいものは別名を付ける
* ○○IDみたいにシステム内で重要度の高い型は opaque type にする

一方、より複雑な構造を持つ型を作成する場合は別の型と取り違える可能性が低いので __レコード型でも十分候補になります__ 。そのため[こちらの解説](https://medium.com/@ckoster22/advanced-types-in-elm-opaque-types-ec5ec3b84ed2)のようなもっと高度な考察をして判断する必要がありそうです。