version
- elm 0.19.0
- elm-form-decoder 1.2.0
はじめに
最近Elmではやりのelm-form-decoderのAPIを1個ずつ見ていきます
(お仕事コードでだいぶ使ってしまったので説明用です)
elm-form-decoderの背景やお気持ちについては作者の「フォームバリデーションからフォームデコーディングの時代へ」の記事をご覧ください
上記記事をみていただければいいんですが軽く説明するとフォームデコーダーはいわゆるバリデーションライブラリの発展形です。**お前はユーザーの入力した値をチェックするだけでいいのか?……違うだろ!欲しいデータ型に変換もするだろ!!**って感じです
「入力をバリデーションしつつ変換する」がやりたいことです
ということでいちからどんなモジュールか調べていきましょう
型
知らないライブラリを調べるときはまず型をみましょう
type Decoder input err a
= Decoder (input -> Result (List err) a)
入力を受け取ってResult (List err) a
を返す関数か……もう完全にわかりましたね。解散です!🐐
デコーダーはある型からある型への変換を表す型なので、左のinput
が入力の型、真ん中のerror
が変換中にエラーになったときのエラーの型、右のa
が変換後の型になります
左から右に流れる感じにみえますね
Decoder
を使う関数
型をみたら次はその型を使って一般的な型に変換する関数を探しましょう。モジュール外との境界面ですね
今回はデコーダーなのでデコードするときに使う関数です
run : Decoder input err a -> input -> Result (List err) a
errors : Decoder input err a -> input -> List err
run
とerrors
の2つありました。run
はinputの値をもらってResult
を返しています。errors
はエラーだけ取れるようになっていますね。
これでDecoder input err a
の使い方がわかりました。とにかくエラーが欲しいときはerrors
、変換したいときはrun
を使えばよいです
プリミティブなデコーダー
このモジュールはデコーダーなので最初に簡単なIntやStringのデコーダーを探しましょう
int : err -> Decoder String err Int
float : err -> Decoder String err Float
intとfloatがあります。この2つはどちらもStringから各数値型への変換するデコーダーになっています
おそらくString.toInt, toFloatで変換していて、失敗した場合は引数のerrを返す実装になっているのでしょう(実装の確認は読者への課題とする)
これだけ見るとString.toIntなどで直接変換すればいいやって気分になりますね。
Primitive decodersのところにあるのはあとは以下の3つです
always : a -> Decoder input never a
identity : Decoder input never input
fail : err -> Decoder input err a
あれれーstringがないぞー
とわたしはなりました。identity
が相当です
Decoder input never input
の型変数をみてみましょう。入力と出力が同じ型でエラーはnever
になっています。つまり入力の値を失敗せずそのまま出力にすると読めます
あくまでそう読めるというだけで
never
は失敗しないというのを型的に保証するものではありません。慣習的な表現です。これに限らず引数で固定されない型変数はNever
入れちゃってもいいんですが変換するのが面倒なのでNever
を入れてないだけです
identityをStringに限定するとこうなります
string : Decoder String never String
string = identity
これでStringからStringに変換する(何もしない)デコーダーです
何もしないで受け流す、つまりidentityですね
あとの2つは定数的なものです
always
は受け取ったa
を変換結果にするデコーダー、fail
は受け取ったerr
で失敗するデコーダーになります
これでプリミティブなデコーダーは終わりです
custom
custom
はカスタムって名前ですがデコーダーの定義そのままです。こわくないよ
custom : (input -> Result (List err) a) -> Decoder input err a
type Decoder input err a
= Decoder (input -> Result (List err) a)
見ての通りそのままですね。フォームデコーダーは失敗するかもしれない変換です
バリデーター
elm-form-decoderにはDecoder
のほかにもう1つ型が定義されています
type alias Validator input err =
Decoder input err ()
さっきから変換だ変換だと書いていますがフォームといえばバリデーションです。
elm-form-decoderのValidator
はDecoder
の変換後の型がないという形で定義されています
つまりinputを受け取って変換はせずにある処理に成功か失敗するかをみています
validator : Int -> Result String ()
validator num =
if num >= 0 then
Ok ()
else
Err "0以上の整数でなければなりません"
きっとこんな感じですね
基本的なバリデーター
4つ用意されています
minBound : err -> comparable -> Validator comparable err
maxBound : err -> comparable -> Validator comparable err
minLength : err -> Int -> Validator String err
maxLength : err -> Int -> Validator String err
4つとも形はほぼ一緒ですね。最初のerr
はそのバリデーターが失敗したときに出るエラー値です
min/maxBound
は最小/最大値で値を制限します。min/maxLength
はStringに対して最小/最大長を制限します
バリデーターの適用方法
作ったバリデーターをデコーダーに適用することでインプットの値を変換しつつバリデーションもするデコーダーを作ることができます
assert : Validator a err -> Decoder input err a -> Decoder input err a
type Error
= Invalid
| TooSmall
| TooBig
myDecoder : Decoder String Error Int
myDecoder =
int Invalid
|> assert (minBound TooSmall 3)
|> assert (maxBound TooBig 6)
デコーダーにパイプしてassert
でバリデーターを適用します
これで文字列を3以上6以下の整数に変換するデコーダーができました
assert
の型定義から、バリデーターが検証するa
の型はデコーダーの変換後の型だとわかります
デコーダーで変換したあとにバリデーターでその値を検証するという感じです
バリデータの関数には他にwhen
とunless
があります
when : (a -> Bool) -> Validator a err -> Validator a err
unless : (a -> Bool) -> Validator a err -> Validator a err
ある条件を満たす(または満たさない)ときだけバリデーターを適用させる、という変換ですね
デコーダーを組み立てる(ここから実践編)
デコーダーで大事なのは小さなデコーダーを組み合わせて大きなデコーダーを作れることです
(あと変換と検証を統一的に扱えることです)
type alias Form =
{ name : String, age : String }
type alias Person =
{ name : String, age : Int }
Form型からPerson型へのデコーダーを定義してみましょう
各フィールドのデコーダーを作る
いきなりForm型からPerson型へのデコーダーを作るのは大変なので、まずnameとageをそれぞれ変換するデコーダーを作りましょう。まず型定義から考えます
nameDecoder : Decoder String err String
ageDecoder : Decoder String err Int
nameDecoder
はStringからStringへの変換、ageDecoder
はStringからIntへの変換です
変換前と後の型を考えたところで制約や失敗するかどうかを考えます
- 未入力はだめ
- StringからIntへの変換で失敗する
- 負の年齢はだめ
これをエラー型にしましょう
type Error
= Required
| IntInvalid
| MinusInt
ネーミングセンスについては置いておきましょう。これらを上から作っていきます
notEmpty : Validator String Error
notEmpty =
minLength Required 1
myInt : Decoder String Error Int
myInt =
int IntInvalid
notMinus : Validator Int Error
notMinus =
minBound MinusInt 0
できました。では名前と年齢のデコーダーを作りましょう
nameDecoder : Decoder String Error String
nameDecoder =
Form.Decoder.identity
|> assert notEmpty
ageDecoder : Decoder String Error Int
ageDecoder =
myInt
|> assert notMinus
できました。(ここの実装に関して問題があるのですが段階を踏んでいる都合上これでいきます。続編で書きます)
Form型に"あげる"
各フィールドのデコーダーを作ったのでこれを組み合わせてFormからPersonへのデコーダーを作りたいです
ということでmapN
関数を使って組み合わせましょう
decoder_ =
map2 Person
nameDecoder
ageDecoder
できました!
嘘です、これは罠です。Decoder Form Error Person
という型を付けると以下のエラーが出ます
Something is off with the body of the `decoder_` definition:
This `map2` call produces:
Decoder String Error Person
But the type annotation on `decoder_` says it should be:
Decoder Form Error Person
定義と型注釈の型があってませんと言われています
入力の型がFormじゃなくてStringみたいです。今までの実装にFormでてきてませんからね……順当です
ということで入力の型をあげましょう。lift
を使います
lift : (j -> i) -> Decoder i err a -> Decoder j err a
感覚的には入力の型を上にあげる感じです
今回の場合各フィールドから上のForm型にあげていきます
nameDecoder_ : Decoder Form Error String
nameDecoder_ =
lift .name nameDecoder
ageDecoder_ : Decoder Form Error Int
ageDecoder_ =
lift .age ageDecoder
nameとageのデコーダーの入力の型をFormにあげました
(liftの定義的に.name、.ageだけでは型が定まりませんが型注釈でFormに固定してます)
.name
の型は{ b | name : a } -> a
です。a
はStringで固定されるので{ b | name : String } -> String
ですね。b
はlift
だけでは固定されません。型注釈で固定してます
Formから対象の値へのgetterをliftに渡すと入力の型がずれます
デコーダーは
input -> Result (List err) a
なのでform -> field
とfield -> Result (List err) a
があればform -> Result (List err) a
が作れます
get : form -> field
fieldDecoder : field -> Result (List err) a
formDecoder : form -> Result (List err) a
formDecoder form =
fieldDecoder (get form)
>
> mapは慣れているかと思いますがliftってなんだよってひともわかりましたか?わたしはフォームデコーダーでわかりました
入力の型をFormにしたので組み合わせてみましょう
```elm
decoder : Decoder Form Error Person
decoder =
map2 Person
nameDecoder_
ageDecoder_
こんどは大丈夫です。これで完成しました!
パイプでつなげて構成する
フォームデコーダーには別スタイルのAPIも用意されています
decoder2 : Decoder Form Error Person
decoder2 =
top Person
|> field nameDecoder_
|> field ageDecoder_
json-decode-pipelineでお馴染みの形ですね
フォームデコーダーではmap5
までしかないのでそれ以上つなげたいときはこっちのスタイルで書くことになります
この
top
はalways
と同じです。定義も一緒です。json-decode-pipelinではsucceed
を使いますね
他の関数
andThen
とwith
ちょっと難しい関数にandThen
とwith
の2つがあります
with : (i -> Decoder i err a) -> Decoder i err a
andThen : (a -> Decoder input x b) -> Decoder input x a -> Decoder input x b
andThen
はMaybeなどにもあるのでわかるんじゃないかと思います。結果の値に依存したデコーダーをつなげる関数です
with
はひとに説明できるほどわかっていないのですが、入力の値に依存したデコーダーを作る関数です
andThen
とwith
は逆の関係にみえます。あんまりよくわかんないんですけど
pass
from 1.2.0
pass : Decoder b x c -> Decoder a x b -> Decoder a x c
デコーダーをシーケンシャルにつなげていく関数です。aからbに変換したあとbからcへのデコーダーでcに変換します。つまりaからcへのデコーダーになります
結果を次に「渡す」の意味っぽいです
クイズ: なんでErrになるか
— 🐐ヤギ魔法少女さくらちゃん(Kadzuya Okamoto) (@arowM_) June 19, 2019
import Form.Decoder as Decoder exposing (Decoder)
pre : Decoder String Error String
pre = Decoder.custom <| Ok << String.filter (\c -> c /= ',')
Decoder.run (pre |> Decoder.andThen (\_ -> Decoder.float Invalid)) "3,000"
--> Err [Invalid]ユーザーからこの手の失敗を報告されたから作ったみたいです。これつまり最初の
pre
で変換した値を捨てているのが問題です。ただのDecoder.float
になってしまっています
これを回避するにはString.toFloat
を直接使ったり、run
をかましてcustom << run Decoder.float Invalid
ってすればいいんですがちょっとやりにくいですね
ということでpass
はandThen
だけでも原理的にできるけどちょっと大変なのでできましたという感じです
その他
list : Decoder a err b -> Decoder (List a) err (List b)
array : Decoder a err b -> Decoder (Array a) err (Array b)
inとoutの両方をList
/Array
にします
mapError : (x -> y) -> Decoder input x a -> Decoder input y a
エラーを変換します
今まででてきた組み合わせ関数ではerrの型は同じにしないといけませんでした。
終わり
elm-form-decoderの関数を紹介してきました
ここから今のわたしのきっちりレベルでフォームを作ろうかと思ったんですがちょっと気力と紙幅の関係上、次に持ち越します