elm-form-decoderの理解のためにelm form decoder katakata を試してみたメモ

## 背景

* [elm form decoder](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/)がよいという記事をいくつか見かけて気になっていた。
  * [arowM/elm-form-decoderのAPIを【かんぜんりかい】しよう！](https://qiita.com/miyamo_madoka/items/d02f003ec1c212360111)
  * [フォームバリデーションからフォームデコーディングの時代へ](https://qiita.com/arowM/items/6c32db1f9e4b92445f3b)
  * [elm-form-decoderでフォームプログラミングにこんにちはする。](https://qiita.com/ababup1192/items/85eb73887d05d2ef3383)

では実際にはどんな感じで使われるのだろうと、サンプルを見にいく。
挫折。 orz

* [サンプル](https://github.com/arowM/elm-form-decoder/blob/master/sample/src/Goat.elm)
  * `Atom.Input`って何？ packageにもないし定義しているとこ見つからない。。。 
     * → `src`フォルダではなく`lib/src`フォルダ以下にいた。
     * `cd lib && npm i && npm start`とドキュメントみれるよと教えていただく。感謝。
  * `Attributes.boolAttribute`、、? ...あー。` Css.classWithPrefix "form__"` 、、? ...へー。

知らない手法がたくさん使われている。
勉強にはなるけれど、気が付いたら、そちらを調べてばかりいる。
もともとのelm form decoderの使い方にまだ辿り着いていない。

もっと簡単なサンプルとして、以下を教えていただいたので試してみる。

* [elm form decoder katakata](https://github.com/miyamoen/elm-form-decoder-katakata)

## 環境

* windows 10
* gitbash
* elm 0.19.1
* vscode
  * [elm拡張](https://marketplace.visualstudio.com/items?itemName=Elmtooling.elm-ls-vscode)

## 準備

elm reactorを使ってねと書かれていた。
elm reactor、、聞いたことはあるが、、。
調べてみたら、elmについてくる実行環境的なものの模様。([elm インストール](https://guide.elm-lang.jp/install.html)) / ( [elm reactorとwebpackを使って快適なElm開発環境を構築する](https://qiita.com/louvre2489/items/e9f8b9d05ea82cf49261))
仮想環境上でwebpackでコンパイルしてたので今まで使ったことなかった。

改めて、windowsにelmをインストールして、試してみる。

```
npm install -g elm elm-format
git clone git@github.com:miyamoen/elm-form-decoder-katakata.git
cd elm-form-decoder-katakata
elm reactor
```

バージョンが違うって怒られた。
以下を編集。

```diff:elm.json
{
  "type": "application",
  "source-directories": [
    "src"
  ],
-  "elm-version": "0.19.0",
+  "elm-version": "0.19.1",
  "dependencies": {
```

```
elm reactor
```

http://localhost:8000/src/KataKata01HelloForm.elm にアクセス。
今度はうまくいった！

## 実践

form decoderに至る前の段階から順を踏んでいく模様。
丁寧。

### KataKata01HelloForm.elm 

reactorで表示されたブラウザのページにアクセスすると、以下の課題が。

* 🆖 textは10文字以下の文字列です

```elm
update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangeText string ->
            -- 何か実装し忘れているような。`replace____me string`を書き換えましょう
            { model | text = replace____me string }
```

`replace___me`という関数を書き換えればよい模様。
stringとは、、と調べると`ChangeText String`ということなので、文字列。
textとは、、`{ text : String }`。

なので、 `replace___me: String -> String`の関数を考えてやればよい。
関数の中身でやりたいことは、入力された文字を10文字以下にすること。
elmではどう書けば、とドキュメントをしらべる。
[String.length](https://package.elm-lang.org/packages/elm/core/latest/String#length)と[String.slice](https://package.elm-lang.org/packages/elm/core/latest/String#slice)が使えそう。

```elm
replace____me : String -> String
replace____me s =
    if String.length s > 10 then
        String.slice 0 10 s

    else
        s
```

クリア！

### KataKata02SeparateModel

今回の課題。

* 🆖 textはformTextから変換されます
* 🆖 formTextが10文字より長いとき、変換は失敗します

```elm 
        ConvertText ->
            { model
              -- `replace____me model.text`を書き換えましょう
                | text = replace____me model.text
            }
```

まずは、型の確認。`{ text : Maybe String, formText : String }` 。
textはformTextから変換されますってあるので、 `text = replace____me model.formText`となる。
なので、今回実装の必要な型は`replace___me: String -> Maybe  String`となる。
[Maybe](https://package.elm-lang.org/packages/elm/core/latest/Maybe#Maybe)は`Just a`と`Nothing`を返せばよいっぽい。

```elm
type Maybe a
    = Just a
    | Nothing
```
よって、以下。

```elm 
replace____me : String -> Maybe String
replace____me s =
    if String.length s > 10 then
        Nothing

    else
        Just s
```

クリア！

### KataKata03IntAndRecord

課題が多い！

* 🆖valueはformから変換されます
  * valueがformから変換されるように`replace____me`を書き換えましょう
* 🆖form.textが10文字より長いとき、変換は失敗します
  * form.textが10文字より長いときは変換が失敗するようにしましょう
* 🆖 form.textが空文字のとき、変換は失敗します
  * form.textが空文字のときは変換が失敗するようにしましょう
* 🆖 form.numberが整数に変換できなければ、変換は失敗します
  * form.numberが整数にできないときは変換が失敗するようにしましょう
* 🆖 form.numberが1以上10以下の整数に変換できなければ、変換は失敗します
  * form.numberが1以上10以下の整数に変換できなければ失敗するようにしましょう

```elm
        ConvertForm ->
            { model
              -- `replace____me model.value`を書き換えましょう
                | value = replace____me model.value
            }
```

課題が多いけれど、順番にやっていこう。
落ち着くんだ。
型の確認。

```elm
type alias Model =
    { value : Result String Value, form : Form }


type alias Value =
    { text : String, number : Int }


type alias Form =
    { text : String, number : String }
```

`value = replace____me model.form`ですね。
型は`replace____me : Form -> Result String Value`。
Resultってなんだっけと[elm Result](https://guide.elm-lang.jp/error_handling/result.html)を読む。

```elm 
type Result error value
  = Ok value
  | Err error
```

今回の`replace____me`は、`OK Value`と`Err String`を返せばよいってことですね。
まずは、前回の経験を活かせる文字列長のチェックから。いったんValueは固定値とする。

```elm
replace____me : Form -> Result String Value
replace____me form =
    let
        len =
            String.length form.text
    in
    if len == 0 then
        Err "必須項目です"

    else if len > 10 then
        Err "10文字以内で入力してください"

    else
        Ok (Value "" 0)
```

* ✅ form.textが10文字より長いとき、変換は失敗します
* ✅ form.textが空文字のとき、変換は失敗します

数値の変換は、[String.toInt](https://package.elm-lang.org/packages/elm/core/latest/String#toInt)が使えそうです。

```elm
replace____me : Form -> Result String Value
replace____me form =
    let
        len =
            String.length form.text
    in
    if len == 0 then
        Err "必須項目です"

    else if len > 10 then
        Err "10文字以内で入力してください"

    else
        case String.toInt form.number of
            Just i ->
                if i > 0 && i < 11 then
                    Ok (Value form.text i)

                else
                    Err "1以上10以下で入力してください"

            Nothing ->
                Err "整数を入力してください"
```

クリア！

#### 追加課題

* 🆖 form.textがnumber文字より長いとき、変換は失敗します
  * form.textがnumber文字より長いときは変換が失敗するようにしましょう

```diff
     if len == 0 then
         Err "必須項目です"


-    else if len > 10 then
-        Err "10文字以内で入力してください"
-
     else
         case String.toInt form.number of
             Just i ->
-                if i > 0 && i < 11 then
+                if len > i then
+                    Err (form.number ++ "文字以内で入力してください")
+
+                else if i > 0 && i < 11 then
                     Ok (Value form.text i)

                 else
```

クリア！

### KataKata04HelloFormDecoder

ここで form-decoder登場！

* 🆖 textはformTextから変換されます
  * textがformTextから変換されるように`replace____me*`を書き換えましょう
    * replace____me1はDecoder.runとdecoderを使ってformTextを変換しましょう
    * replace____me2はDecoder.identityで`Decoder String err Strig`を作りましょう
* ✅ formTextが空文字のとき、変換は失敗します
* 🆖 formTextが10文字より長いとき、変換は失敗します
  * formTextが10文字以上のときはTextTooLongのエラーで失敗するようにdecoderにバリデーションを足しましょう

```elm
update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangeText string ->
            { model | formText = string }

        ConvertText ->
            -- `replace____me1 model.value`を書き換えましょう
            replace____me1 model

decoder : Decoder String Error String
decoder =
    -- `replace____me2 <| Decoder.always "虎にならない"`を書き換えましょう
    replace____me2 <| Decoder.fail TextEmpty
```

型の確認。

```
type alias Model =
    { text : String, formText : String }
```

* `replace____me1: Model -> Model`
* `replace____me2: Decoder String Error String`

小さいほうから。
`replace____me2`はDecoder.identityを使えと書いてあります。
[elm form decoder](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/)を見に行って、identityで検索したら、以下がヒット。

```elm
name : Decoder String Error String
name =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength NameRequired 1)
```

そのまま使えそうですね。

```elm 
replace____me2 : Decoder String Error String
replace____me2 =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength TextEmpty 1)
```

で、これ何やってるの？
キーワードの型を[調べて](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/Form-Decoder)並べてみる。

* `identity : Decoder input never input`
* `assert : Validator a err -> Decoder input err a -> Decoder input err a`
* `minLength : err -> Int -> Validator String err`

`assert`は２つの引数をとると。
今回の場合

* 1つ目の引数は、`(Decoder.minLength TextEmpty 1)`。型は`Validator String Error`。
  * `a = String`、`err = Error`
* 2つ目の引数は、`Decoder.identity |>` 。 `identity`の型は`Decoder input never input`
  * `assert`の2つ目の引数なので、`Decoder a err a`となる
  * 1つめの引数により、`a = String, err = Error`なので、`Decoder String Error String`  
* `Validator String Error -> Decoder String Error String -> Decoder String Error String `となる。

こんな感じかなー。
これで以下はOK。

*  ✅formTextが空文字のとき、変換は失敗します

次は以下のエラーを消すために、バリデータを足す。

* 🆖 formTextが10文字より長いとき、変換は失敗します

Decoder.maxLength もあるようなので、それを使用する。
`assert: Validator a err -> Decoder input err a -> Decoder input err a`の2つ目の引数と戻り値に注目。
`Decoder input err a `を受け取って、`Decoder input err a `を返すことができる。
1つ目の空文字で失敗するDecoderに、
2つ目の10文字以上はエラーとする処理を加えて新たな`Decoder`にすることができる。
要するに、以下。


```diff
replace____me2 : Decoder String Error String
replace____me2 =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength TextEmpty 1)
+        |> Decoder.assert (Decoder.maxLength TextTooLong 10)
```

最後に、変換。
ヒントに`run`の結果は`Result (List err) a`なのでパターンマッチで欲しい値を手に入れるとあったので、以下。

```elm 
replace____me1 : Model -> Model
replace____me1 model =
    case Decoder.run decoder model.formText of
        Ok text ->
            { model | text = text }

        Err errorList ->
            model
```

クリア！

### KataKata05BuildUpDecoder

NGがいっぱい、、、！
気圧されそうになるけれど、一つ一つ片づけてゆく。

まずは、型の確認から、の前に、今回学ぶことを確認。

> \## 今回学ぶこと
>   - Int型のDecoder
>       - Decoder.intで`Decoder String err Int`が作れる
>   - 最大値を最小値のバリデーションを適用する
>       - minBoundとmaxBoundでできる
>   - Decoderの組み合わせ方
>       - top, fieldを使った組み合わせ方
>       - liftを使って入力の型を合わせる方法

上部コメントに、`.lift`の説明なども書いてあるので読む。

型の確認。

```elm
type alias Model =
    { value : Value, form : Form }


type alias Value =
    { text : String, number : Int }


type alias Form =
    { text : String, number : String }
```

```elm
decoder : Decoder Form Error Value
decoder =
    -- この行全体を書き換えましょう
    replace____me3 <| Decoder.always { text = "initial", number = 1 }


textDecoder : Decoder String Error String
textDecoder =
    -- katakata 04のdecoderの実装をそのまま使いましょう
    replace____me1 <| Decoder.always "虎にならない"


numberDecoder : Decoder String Error Int
numberDecoder =
    -- この行全体を書き換えましょう
    replace____me2 <| Decoder.always -5
```

まずは`textDecoder`を置き換え。

```elm
textDecoder : Decoder String Error String
textDecoder =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength TextEmpty 1)
        |> Decoder.assert (Decoder.maxLength TextTooLong 10)
```

次に、[`Decoder.int`の型](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/Form-Decoder#array)を確認。`int : err -> Decoder String err Int`
`Decoder.identity`は`identity : Decoder input never input`だったので、比較すると、引数のerrを1つ追加してやる必要がある。
よって、以下。

```elm
numberDecoder : Decoder String Error Int
numberDecoder =
    Decoder.int NumberInvalidInt
        |> Decoder.assert (Decoder.minBound NumberBelow 1)
        |> Decoder.assert (Decoder.maxBound NumberOver 10)
```

つぎに、この２つのdecoderを組み合わせる。
上部コメントに、以下のサンプルがあるので、それに倣う。

```elm 
    decoder : Decoder TigerForm Error Tiger
    decoder =
        Decoder.top Tiger
            |> Decoder.field (Decoder.lift .name nameDecoder)
            |> Decoder.field (Decoder.lift .species speciesDecoder)
```

```elm
decoder : Decoder Form Error Value
decoder =
    Decoder.top Value
        |> Decoder.field (Decoder.lift .text textDecoder)
        |> Decoder.field (Decoder.lift .number numberDecoder)
```

クリア！

#### 追加課題
文字列の長さはnumber以下にすることにしました。つまり、numberは1以上10以下の整数でtextはnumber文字以下の空文字ではない文字列です。

まずは考え方を読む。


> Validatorを作る専用APIはありませんが、Validatorの定義をみれば作り方はわかると思います。
> `i -> Result (List err) ()`を作ればいいわけです。`custom`を使えば作ることができます。
> 
>     custom : (input -> Result (List err) a) -> Decoder input err a
> 
>     tigerValidator : Validator Tiger Err
>     tigerValidator =
>         custom <|
>             \tiger ->
>                 if hoge tiger then
>                     Ok ()
> 
>                 else
>                     Err [ TigerTooBig ]

> このようにデコードしたあとの虎を使っていくらでもバリデーションできます。

[Decoder.custom](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/Form-Decoder#custom)を確認。
`custom <| \n ->`が混乱したが、よく見ると上でやってることと変わらない。
`custom`の第一引数には`input -> Result (List err) a`を渡せばよくて、それを無名関数で作って渡してるだけね。
`<| ->`が一行にあるのと、`\n`が改行コードがまず思い浮かんでしまうので混乱したっぽい。閑話休題。
回答は以下。

```elm
valueValidator : Decoder.Validator Value Error
valueValidator =
    Decoder.custom <|
        \value ->
            if String.length value.text < value.number then
                Ok ()

            else
                Err [ TextTooLong ]
```

後は、これを組み合わせてやるだけ。


```diff
decoder : Decoder Form Error Value
decoder =
    Decoder.top Value
        |> Decoder.field (Decoder.lift .text textDecoder)
        |> Decoder.field (Decoder.lift .number numberDecoder)
+        |> Decoder.assert valueValidator
```

クリア！

## バリデータを使っている部分を見る

これで、使い方は分かった。
あとは、それを出力している部分だが。。

```elm
    let
        errors =
            Decoder.errors decoder form
    in
            Form.view []
                [ Form.text
                    { label = "text"
                    , onChange = ChangeText
                    , attrs = [ htmlAttribute <| Html.Attributes.autofocus True ]
                    , form = form.text
                    , value = value.text
                    }
                , Form.errors
                    [ ( "必須です", List.member TextEmpty errors )
                    , ( "10文字以下にしてください", List.member TextTooLong errors )
                    ]
                , Form.text
                    { label = "number"
                    , onChange = ChangeNumber
                    , attrs = []
                    , form = form.number
                    , value = String.fromInt value.number
                    }
                , Form.errors
                    [ ( "整数を入力してください", List.member NumberInvalidInt errors )
                    , ( "1以上の整数を入力してください", List.member NumberBelow errors )
                    , ( "10以下の整数を入力してください", List.member NumberOver errors )
                    ]
                , Form.button
                    { label = "変換する"
                    , msg = ConvertForm
                    , enable = List.isEmpty errors
                    }
                ]
```

`Form.erros`ってなんじゃろう。
Componentフォルダにあった。

```elm
errors : List ( String, Bool ) -> List (Element msg)
errors messages =
    [ none
    , column [ spacing 12, paddingXY 12 0, Font.size 16, Font.color <| rgb255 234 122 184 ] <|
        List.map
            (\( message, invalid ) ->
                if invalid then
                    Element.text message

                else
                    none
            )
            messages
    , none
    , none
    ]
```

Elementとは、、？
[elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/Element)の部品ぽい。
ここからは、form decoderでなくて、elm uiのほうのことになりそうなので割愛。
`Decoder.errors decoder form`で取り出して、`List.member`で表示/非表示の切り替えをすれば良さそう。