5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

5
Last updated at Posted at 2019-12-14

背景

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

  • サンプル
    • Atom.Inputって何? packageにもないし定義しているとこ見つからない。。。
      • srcフォルダではなくlib/srcフォルダ以下にいた。
      • cd lib && npm i && npm startとドキュメントみれるよと教えていただく。感謝。
    • Attributes.boolAttribute、、? ...あー。 Css.classWithPrefix "form__" 、、? ...へー。

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

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

環境

  • windows 10
  • gitbash
  • elm 0.19.1
  • vscode

準備

elm reactorを使ってねと書かれていた。
elm reactor、、聞いたことはあるが、、。
調べてみたら、elmについてくる実行環境的なものの模様。(elm インストール) / ( elm reactorとwebpackを使って快適なElm開発環境を構築する)
仮想環境上で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

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

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文字以下の文字列です
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.lengthString.sliceが使えそう。

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文字より長いとき、変換は失敗します
        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となる。
MaybeJust aNothingを返せばよいっぽい。

type Maybe a
    = Just a
    | Nothing

よって、以下。

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以下の整数に変換できなければ失敗するようにしましょう
        ConvertForm ->
            { model
              -- `replace____me model.value`を書き換えましょう
                | value = replace____me model.value
            }

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

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を読む。

type Result error value
  = Ok value
  | Err error

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

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が使えそうです。

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文字より長いときは変換が失敗するようにしましょう
     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にバリデーションを足しましょう
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を見に行って、identityで検索したら、以下がヒット。

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

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

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

で、これ何やってるの?
キーワードの型を調べて並べてみる。

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

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

  • 1つ目の引数は、(Decoder.minLength TextEmpty 1)。型はValidator String Error
    • a = Stringerr = 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にすることができる。
要するに、以下。

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なのでパターンマッチで欲しい値を手に入れるとあったので、以下。

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の説明なども書いてあるので読む。

型の確認。

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


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


type alias Form =
    { text : String, number : String }
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を置き換え。

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

次に、Decoder.intの型を確認。int : err -> Decoder String err Int
Decoder.identityidentity : Decoder input never inputだったので、比較すると、引数のerrを1つ追加してやる必要がある。
よって、以下。

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

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

    decoder : Decoder TigerForm Error Tiger
    decoder =
        Decoder.top Tiger
            |> Decoder.field (Decoder.lift .name nameDecoder)
            |> Decoder.field (Decoder.lift .species speciesDecoder)
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を確認。
custom <| \n ->が混乱したが、よく見ると上でやってることと変わらない。
customの第一引数にはinput -> Result (List err) aを渡せばよくて、それを無名関数で作って渡してるだけね。
<| ->が一行にあるのと、\nが改行コードがまず思い浮かんでしまうので混乱したっぽい。閑話休題。
回答は以下。

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

            else
                Err [ TextTooLong ]

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

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

クリア!

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

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

    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フォルダにあった。

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の部品ぽい。
ここからは、form decoderでなくて、elm uiのほうのことになりそうなので割愛。
Decoder.errors decoder formで取り出して、List.memberで表示/非表示の切り替えをすれば良さそう。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?