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の関数を紹介してきました
ここから今のわたしのきっちりレベルでフォームを作ろうかと思ったんですがちょっと気力と紙幅の関係上、次に持ち越します