20
7

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.

arowM/elm-form-decoderのAPIを【かんぜんりかい】しよう!

Last updated at Posted at 2019-06-21

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

runerrorsの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のValidatorDecoderの変換後の型がないという形で定義されています
つまり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の型はデコーダーの変換後の型だとわかります
デコーダーで変換したあとにバリデーターでその値を検証するという感じです

バリデータの関数には他にwhenunlessがあります

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ですね。bliftだけでは固定されません。型注釈で固定してます

Formから対象の値へのgetterをliftに渡すと入力の型がずれます

デコーダーはinput -> Result (List err) aなのでform -> fieldfield -> 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までしかないのでそれ以上つなげたいときはこっちのスタイルで書くことになります

このtopalwaysと同じです。定義も一緒です。json-decode-pipelinではsucceedを使いますね

他の関数

andThenwith

ちょっと難しい関数にandThenwithの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はひとに説明できるほどわかっていないのですが、入力の値に依存したデコーダーを作る関数です

andThenwithは逆の関係にみえます。あんまりよくわかんないんですけど

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へのデコーダーになります
結果を次に「渡す」の意味っぽいです

ユーザーからこの手の失敗を報告されたから作ったみたいです。これつまり最初のpreで変換した値を捨てているのが問題です。ただのDecoder.floatになってしまっています
これを回避するにはString.toFloatを直接使ったり、runをかましてcustom << run Decoder.float Invalidってすればいいんですがちょっとやりにくいですね
ということでpassandThenだけでも原理的にできるけどちょっと大変なのでできましたという感じです

その他

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の関数を紹介してきました

ここから今のわたしのきっちりレベルでフォームを作ろうかと思ったんですがちょっと気力と紙幅の関係上、次に持ち越します

20
7
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
20
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?