[Elm] CustomTypeを活用する

## 背景
- CustomTypeに引数を渡すことで状態管理がより適切にできるはず
- CustomTypeを使わずに作ったものを、CustomTypeを使ってリファクタリングすることでそのメリットを学ぶ

## 作ってみたもの
基本的なTodoリスト
![](https://i.imgur.com/yj8JImo.gif)


### 仕様
- TODO追加ボタン押下で、TODO追加フォームが開く
- 追加フォーム内の追加するボタン押下で、新規TODOが追加され追加フォームが閉じる
- 登録済みTODOのテキストをクリックすると、TODO編集フォームが開く
- 編集フォーム内の保存するボタン押下で、TODOが更新され編集フォームが閉じる
- 追加/編集フォーム内のやめるボタン押下で、追加/編集を取りやめフォームが閉じる
- 追加/編集フォームが開いている時、一度閉じてからでなくても連続して他のフォームを開くことができる
- チェックボックス押下でチェックのON/OFFを変えることができる

## リファクタ前のコード
Elm始めて1ヶ月くらいの時期に書いていたコードはこんな感じ

#### Model

```elm
type alias Model =
    { todos : List Todo
    , addFormS : FormS
    , addingTodo : Todo
    , editFormS : FormS
    , editingTodo : Todo
    }

init : ( Model, Cmd Msg )
init =
    ( { todos =
            [ { id = "xxx1"
              , text = "住みたい街を決める"
              , done = True
              }
            , { id = "xxx2"
              , text = "物件候補を探す"
              , done = False
              }
            , { id = "xxx3"
              , text = "内見の予約を取る"
              , done = False
              }
            ]
      , addFormS = Close
      , editFormS = Close
      }
    , Cmd.none
    )
```

#### Todo

```elm
type alias Todo =
    { id : Id   -- 可読性を上げるためにtype aliasで別名指定した普通のString
    , text : String
    , done : Bool
    }
    
init : Todo
init =
    { id = ""
    , text = ""
    , done = False
    }
```

#### FormS
- uuid生成をjs側で行なっているので、一度Portを経由してjsからuuidを受け取るまでの間の状態をSendingとした。

```elm
type FormS
    = Open
    | Sending
    | Close
```

#### Msg

```elm
type Msg
    = OpenAddTodoForm
    | ChangeAddTodoText String
    | AddTodo
    | AddedTodo Todo
    | CancelAddTodo
    | OpenEditTodoForm Todo
    | ChangeEditTodoText String
    | UpdateTodoText
    | CancelUpdateTodoText
    | UpdateTodoDone Id
```

#### Update

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        OpenAddTodoForm ->
            ( { model | addFormS = Open, addingTodo = Todo.init }, Cmd.none )

        ChangeAddTodoText text ->
            let
                addingTodo =
                    model.addingTodo

                nextAddingTodo =
                    { addingTodo | text = text }
            in
            ( { model | addingTodo = nextAddingTodo }, Cmd.none )

        AddTodo ->
            ( { model | addFormS = Sending }, addTodo { text = model.addingTodo.text } )

        AddedTodo newTodo ->
            let
                nextTodos =
                    model.todos ++ [ newTodo ]
            in
            ( { model | todos = nextTodos, addFormS = Close, addingTodo = Todo.init }, Cmd.none )

        CancelAddTodo ->
            ( { model | addFormS = Close, addingTodo = Todo.init }, Cmd.none )

        OpenEditTodoForm todo ->
            ( { model | editFormS = Open, editingTodo = todo }, Cmd.none )

        ChangeEditTodoText text ->
            let
                editingTodo =
                    model.editingTodo

                nextEditingTodo =
                    { editingTodo | text = text }
            in
            ( { model | editingTodo = nextEditingTodo }, Cmd.none )

        UpdateTodoText ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateText model.editingTodo.id model.editingTodo.text todo) model.todos
            in
            ( { model | todos = nextTodos, editFormS = Close, editingTodo = Todo.init }, Cmd.none )

        CancelUpdateTodoText ->
            ( { model | editFormS = Close, editingTodo = Todo.init }, Cmd.none )

        UpdateTodoDone id ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateDone id todo) model.todos
            in
            ( { model | todos = nextTodos }, Cmd.none )
```

### いけてないところ

#### 追加中の新規TODOや編集中のTODOなど、「追加中」「編集中」という特定の状態のみでしか使われない変数を、一時的な値をmodelに直接持たせている

- Update内でこれらを初期化する記述が頻出。初期化漏れによるバグの危険大

```elm
{ model | addFormS = Close, addingTodo = Todo.init }
{ model | editFormS = Close, editingTodo = Todo.init }
```

- 以下のような意図しない更新ができてしまう

```elm
-- 編集中フォームを閉じたときに追加中TODOが初期化されるという期待しない処理が書けてしまう
{ model | editFormS = Close, addingTodo = Todo.init }
```

- 一時的な値をmodelに持たせていることで、modelの視認性が下がっている


## リファクタ後のコード
- Elm3ヶ月くらいになった自分が過去の自分のコードをリファクタする
- CustumTypeの引数を活用して、この問題を解決してみる

#### Model
- めっちゃスッキリしてわかりやすいModelになった

```elm
type alias Model =
    { todos : List Todo
    , addFormS : AddFormS
    , editFormS : EditFormS
    }
```

#### FormS
- AddとEditで分けてみた
- リファクタ前はModelに直接追加中のTODOや編集中Todoの情報を持たせていたが、これらはフォームが開いている状態の時のみ持てばいい情報なので、OpenのCustomTypeの引数に持たせてみた

```elm
type AddFormS
    = AddFormOpen Todo
    | Sending
    | AddFormClose

type EditFormS
    = EditFormOpen Todo
    | EditFormClose
```

- リファクタ前のコードでは、Formの状態と追加中(or編集中)のTODOの状態を別々の変数で持っていたため、Formの状態に関係なくTODOを書き換えたり初期化したりできてしまっていた。例えば追加中TODOのテキスト更新(ChangeAddTodoText)でeddingTodoの上書きができてしまうような実装になっていた。
- CustumTypeの引数に追加中(or編集中)のTODOを持たせることで、フォームが開いている状態のみ追加中(or編集中)TODOの状態を扱えるようになり、よりわかりやすく仕様に合った実装になった。

```diff
OpenAddTodoForm ->
-    ( { model | addFormS = Open, addingTodo = Todo.init }, Cmd.none )
+    ( { model | addFormS = AddFormOpen Todo.init }, Cmd.none )
```


#### update
- Modelはスッキリしたがupdateの記述量は増えた（CustumTypeの引数をcase文で取り出す部分でコード量が増えている）
- 記述量は増えたが、updateで何をしようとしているのか、どんな状態更新をしようとしているのかがわかりやすくなった

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        OpenAddTodoForm ->
            ( { model | addFormS = AddFormOpen Todo.init }, Cmd.none )

        ChangeAddTodoText text ->
            case model.addFormS of
                AddFormOpen newTodo ->
                    ( { model | addFormS = AddFormOpen { newTodo | text = text } }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        AddTodo ->
            case model.addFormS of
                AddFormOpen newTodo ->
                    ( { model | addFormS = Sending }, addTodo newTodo )

                _ ->
                    ( model, Cmd.none )

        AddedTodo newTodo ->
            let
                nextTodos =
                    model.todos ++ [ newTodo ]
            in
            ( { model | todos = nextTodos, addFormS = AddFormClose }, Cmd.none )

        CancelAddTodo ->
            ( { model | addFormS = AddFormClose }, Cmd.none )

        OpenEditTodoForm todo ->
            ( { model | editFormS = EditFormOpen todo }, Cmd.none )

        ChangeEditTodoText text ->
            case model.editFormS of
                EditFormOpen newTodo ->
                    ( { model | editFormS = EditFormOpen { newTodo | text = text } }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        UpdateTodoText ->
            case model.editFormS of
                EditFormOpen newTodo ->
                    let
                        nextTodos =
                            List.map (\todo -> Todo.updateText newTodo.id newTodo.text todo) model.todos
                    in
                    ( { model | todos = nextTodos, editFormS = EditFormClose }, Cmd.none )

                _ ->
                    ( model, Cmd.none )

        CancelUpdateTodoText ->
            ( { model | editFormS = EditFormClose }, Cmd.none )

        UpdateTodoDone id ->
            let
                nextTodos =
                    List.map (\todo -> Todo.updateDone id todo) model.todos
            in
            ( { model | todos = nextTodos }, Cmd.none )
```

## まとめ
- 全部Modelに変数持たせようとすると煩雑になる
- ある特定の状態でしか必要としない変数は、CustomTypeでその状態を定義し、その引数に持たせることで、ModelやUpdateがわかりやすくなる
- 全体的に使用する変数はもちろん、Modelに持たせてよい