フォームバリデーションからフォームデコーディングの時代へ


GUIのアプリケーションにおいて、ユーザーが入力した内容を検証する「フォームバリデーション」は必須の技術です。

しかし、静的型付けの恩恵を受けるために Flow, TypeScript, Elm, PureScript などをつかってフロントエンドコードを書くようになると、**実はフォームバリデーションだけでは足りなくなってきます**。[^1]

この記事ではフォームバリデーションを包括した新しい概念である **フォームデコーディング** を紹介し、Elmでフォームデコーディングを実践するために開発した elm-form-decoder を使った実例をお見せします。
軽く調べた限りではフォームの入力検証について同等の概念を実現するライブラリなどは見当たりませんでしたが、もし似たライブラリなどをご存じの方がいらっしゃればご指摘いただけると幸いです。[^2]

## サンプルアプリ

まずは現実にありそうな簡単なアプリケーションを題材にしてみましょう。
ヤギさんたちのためのSNSサービスを作ります。

このサービスを実現するには、ヤギさんの2本の指でキーボードをうまく打てるかどうかという問題を解消する必要がありますが、いったんその問題は先送りにすることとします。

また、ここでは簡単に

* ヤギさんをあたらしく登録するフォーム
* 登録されたヤギさんたちの情報を閲覧するページ

の2画面構成とします。

[デモアプリ](https://arowm.github.io/elm-form-decoder/)を先に用意しておきましたので、イメージをよりわかりやすくするために一度遊んでみていただければと思います。

![Screen Shot 2019-04-21 at 15.29.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16345/c9a6cea1-e867-f834-64d4-ae327851c2a8.png)


## データ管理用の型と、フォームの状態管理用の型

さて、このアプリではヤギさんは以下の情報をプロフィールとして保持しています。

* なまえ
* ねんれい
* つの の ほんすう
* れんらく ほうほう
    * めーるあどれす か でんわばんごう
* ほかの やぎさん への めっせーじ
    * かかなくてもいいよ！

静的型を活かしてこのデータを管理するには、たとえば以下のような型を定義して使います。
（この記事ではサンプルコードを Elm で書きます）

```elm
type alias Goat =
    { name : Name
    , age : Age
    , horns : Horns
    , contact : Contact
    , message : Maybe Message
    }
```

`Name` `Age` `Horns` `Message` などの実態は、単に

```elm
type Name
    = Name String

type Age
    = Age Int
```

のように別の場所で定義されていて、[値が制約を満たしていることを保証する](https://qiita.com/arowM/items/0106f2b66a6a446bad38)ために使っています。

`Contact` はメールか電話番号なので

```elm
type Contact
    = ContactEmail Email
    | ContactPhone Phone
```

のように定義しています。

また、`message` は任意項目なので `Nothing` (未入力) または `Just "Hi! I'm Sakura-chan."` のような形式のどちらにも対応できるように `Maybe` にしています。

とてもうまくモデル化できていますね？！


さて、ではこの型でヤギさんの登録フォームの状態を管理してみましょう。
......
......
![p2017.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16345/50379b06-25c3-0e84-5996-7fe61e4b7c4e.jpeg)

それはむずかしいのです。
だって、ユーザーの入力はプレインテキストなのですから...
たとえば年齢の入力欄にユーザー（ヤギさん）が「にさい」と打ち込んだとします。

> このフィールドは整数のみ入力可能です。「にさい」は整数ではありません。

と表示したくても、年齢の情報を `Int`型で保持している `Goat`型では現在の入力値を保持できないので、何らかの方法で直接 input タグの value 値を読み込む必要があります。
Elm ではこういう処理をアンチパターンとして最初からできなくしてありますが、他の言語やフレームワークでも設計を悪くする原因になりえます。[^3]

また、連絡方法のフィールドについてはどうでしょうか？
メールの入力欄と電話番号の入力欄を両方とも最初から表示しておくと、ヤギさんは
「あれ？ どちらかひとつじゃないの？ ぶめめぇ...」
と混乱してしまいます。

それを避けるために、「連絡方法」として

* めーるあどれす
* でんわばんごう

のどちらかを選ばせる選択肢だけを先に表示しておいて、一方を選んだ段階で初めてメールアドレスの入力欄または電話番号の入力欄を表示するようにします。
では、優柔不断なヤギさんが「めーるあどれす」を選んでメールアドレスの入力欄に入れた後で、やっぱり「でんわばんごう」を選び直したあとで「う〜ん、やっぱり めーるあどれす がいい！」と戻したとします。
このときに前回入力したメールアドレスが残っていると親切な設計だと言えます。
しかし、`Goat` 型では連絡方法を `Contact` 型で管理しており、最初にメールアドレスを入力した際に
`UseEmail "you-goat-a-mail@example.com"` として保存されたあとで電話番号を選択すると `UsePhone ""` で上書きされてしまい、もとのメールアドレスは消えてしまいます。

このように、**フォームの状態を管理するのに `Goat` 型は不適切**なのです。

## フォームデコーディングの必要性

では、`Goat` 型とは別に、登録フォームの状態を管理するためだけの `RegisterForm` という型を定義してみましょう。

```elm
type alias RegisterForm =
    { name : String
    , age : String
    , horns : String
    , email : String
    , phone : String
    , contactType : String
    , message : String
    }
```

愚直です。実に愚直です。ぐちょぐちょ愚直です。
ユーザーが入力した値をそのまま文字列として各フィールドに保持するだけです。

`contactType` は連絡方法の選択値を保持するものなので列挙型を定義して

```elm
type ContactType
    = UseEmail
    | UsePhone
```

この `ContactType` にしても良さそうな気もしますが、残念ながら HTML の `select` タグは融通が効きません。
`selectTag.value` には文字列が入っています。
その値をそのまま保持するために `contactType` も `String` 型にしています。

すでにお気づきとは思いますが、この時点で **`RegisterForm` を `Goat` に変換する方法が必要**です。
ユーザーの入力が終わったあとも `RegisterForm` 型でヤギさんのプロフィール情報を管理していたのでは、せっかくの**型の恩恵を受けられない**どころか、
実質動的型付けと変わらない安全性で無駄に型をごちゃごちゃしないといけなくなって、**最初から動的型付けで作ったほうが良くなって**しまいます。

このように、ユーザーが入力した文字列ベースの情報（文字列にエンコードされていると見なす）をデコードして、制約を満たすことが保証されている型に変換する処理を **フォームデコーディング** と呼んでいます。

## フォームデコーディングはフォームバリデーションを包括する

賢い読者の方は思ったはずです。
「いや、フォームデコーディングが必要なのはわかったけど、別にフォームバリデーションを置き換えるものじゃなくね？」と。

そうです。ここまでの話ではフォームデコーディングはフォームバリデーションとはまた別の目的の技術です。
でも、フォームデコーディングが失敗する可能性があることを考えると話が変わってきます。

たとえば、ユーザーの入力状態として、以下の値が `RegisterForm` に保持されているとします。

```elm
type alias RegisterForm =
    { name = "さくらちゃんは"
    , age = "かしこいので"
    , horns = "SNSもつかえるよ"
    , email = ""
    , phone = ""
    , contactType = ""
    , message = "れんらく ほうほう ってなに？"
    }
```

これは明らかに `Goat` 型に変換できません。
まず年齢に数字が入っていないので

```elm
type Age
    = Age Int
```

と定義された `Age` 型で表現できませんし、必須項目の連絡方法も指定されていません。
もちろん、これはいいことで、`Goat` 型の強い制約によって不正な値を作ることができなくするのに成功しているのです。

このことを考えると、`RegisterForm` から `Goat` への変換関数はたとえば以下のような型を持ちます。

```elm
toGoat : RegisterForm -> Maybe Goat
```

不正な入力値の時には `Nothing` が返される関数です。

あるいは以下のように、失敗する理由まで丁寧に教えてくれる関数にしてもいいでしょう。

```elm
type Error
    = NameRequired
    | AgeInvalidInt
    | AgeNegative
    | AgeRequired
    ...
    ...

toGoat2 : RegisterForm -> Result (List Error) Goat
```

入力値が不正な場合は

```elm
Err [ NameRequired, AgeInvalidInt ]
```

のように不正である理由を示し、正しい場合のみ

```elm
Ok (Goat "Sakura-chan" 2 ......)
```

と、`Ok` でくるんで変換後の値を返してくれます。

ここまで見ると、もうこれはフォームバリデーションも行っていることに気づくはずです。
このように、**フォームデコーディングはフォームバリデーションを包括する概念** なのです。

## フォームバリデーションと別にあつかうことの問題点

もちろん、「使い慣れたフォームバリデーションライブラリを使いたい」などの理由で、フォームバリデーションとフォームデコーディングを別々に行いたいこともあるでしょう。
しかし、それには大きな問題があります。

**1つ目は、単純に2度手間になること。**
結局バリデーションのために書いたコードとほとんど同じコードをデコーディングのために書かないといけません。

**2つ目はもっと深刻な問題で、エラーの原因になること。**
別々に同じようなコードを書いていると、

* バリデーションは通るけど
* デコードには失敗する

ようなシチュエーションが発生しかねません。

たとえば以下のような困ったコードになります。

```elm
type alias Model =
    { registerForm : RegisterForm
    , goats : List Goat
    }


{-| バリデーション用の関数
-}
errors : RegisterForm -> List Error
errors = ...


{-| デコード用の関数
-}
toGoat : RegisterForm -> Maybe Goat
toGoat = ...


{-| 「登録」ボタンを押した時に呼ばれてモデルの状態を更新する関数
    先にバリデーション関数で入力値をチェックしているので、不正な入力のときには呼ばれない
-}
onSubmit : Model -> Model
onSubmit model =
    if List.isEmpty (errors model.registerForm) then
        let
            goat : Maybe Goat
            goat = toGoat model.registerForm
        in
        -- うわぁ。バリデーション成功してるのにGoatに変換できないときどうしたらいいんだぁっ
        ...
        ...
```

ということで、**これからの時代はフォームバリデーションではなくフォームデコーディングが必要なのです**。

## elm-form-decoder のご紹介

フォームデコーディングを実現しようと思うと、1点重要な点があります。
それは「**部分的なデコード関数を組み合わせて大きなデコード関数を組み立てられること**」です。
言葉で説明してもよくわからないので、ここで僕が作った [elm-form-decoder](https://package.elm-lang.org/packages/arowM/elm-form-decoder/latest/) を使いながら実例を見ていきましょう。

なお、ここで取り上げるものは説明を簡単にするためにいくつか僕が実際にアプリ開発するときよりも単純にしてあります。
[本気で作った場合のサンプル](https://github.com/arowM/elm-form-decoder/tree/master/sample)もあるので、興味がでたら見てみてください。

実際のフォームでは各入力欄ごとに、その入力欄の直下にその入力欄のエラーを表示することが多いです。
ちょうど[冒頭で挙げたサンプル](https://arowm.github.io/elm-form-decoder/#goat-registerForm)も、たとえば "Age" のところに "foo" などと入力してフォーカスを外すと "Invalid input. Please input integer." と直下にエラーが表示されます。

これを実現するためには、まずフィールドごとにデコーディング用の関数を作る必要があります。
まずは理解できなくてもいいのでなんとなく以下のコードに目を通してください。

```elm
import Form.Decoder as Decoder

{-| Decoder for name field.

    import Form.Decoder as Decoder

    Decoder.run name ""
    --> Err [ NameRequired ]

    Decoder.run name "foo"
    --> Ok "foo"
-}
name : Decoder String Error String
name =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength NameRequired 1)


{-| Decoder for age field.

    import Form.Decoder as Decoder

    Decoder.run age ""
    --> Err [ AgeRequired ]

    Decoder.run age "foo"
    --> Err [ AgeInvalidInt ]

    Decoder.run age "-30"
    --> Err [ AgeNegative ]

    Decoder.run age "30"
    --> Ok 30

-}
age : Decoder String Error Int
age =
    Decoder.identity
        |> Decoder.assert (Decoder.minLength AgeRequired 1)
        |> Decoder.pass (Decoder.int AgeInvalidInt)
        |> Decoder.assert (Decoder.minBound AgeNegative 0)
```

まず注目すべきは、`name` や `age` の型が

```elm
name : String -> Result Error String
age : String -> Result Error Int
```

などではないことです。

代わりに以下のような型になっています。

```elm
name : Decoder String Error String
age : Decoder String Error Int
```

ここで定義されている `name` や `age` は
**デコードを行う関数それ自身ではなく、デコードの方法を記した指南書**なのです。

`Decoder input err a` という型の指南書は、`input` 型の値を `a` 型にデコードするもので、入力値が不正な場合は `err` 型のエラーを返します。

この指南書の通りに入力値をデコードするには、elm-form-decoder が用意している

```elm
run : Decoder input err a -> input -> Result (List err) a
```

を使って

```elm
Decoder.run age ""
--> Err [ AgeRequired ]

Decoder.run age "30"
--> Ok 30
```

のように使います。
なぜ直接デコード用の関数にするのではなく、デコードの方法を記した指南書を作っているかというと、
先に述べた「**部分的なデコード関数を組み合わせて大きなデコード関数を組み立てられる**」ためです。

まずは、先ほど定義した指南書を使って新しい指南書を作ってみましょう。
`name` や `age` は `String`型の値を入力としてとっていましたが、それらを使って`RegisterForm`型の値を入力にするデコーダーを作ってみます。

```elm
name_ : Decoder RegisterForm Error String
name_ = Decoder.lift .name name

age_ : Decoder RegisterForm Error Int
age_ = Decoder.lift .age age
```

単に `RegisterForm` 型の値から対象のフィールドの値を取り出す `.name` や `.age` を与えて `Decoder.lift` を使っているだけです。

ここで定義した `age_` や `name_` などを組み合わせることで、`RegisterForm` を `Goat` に変換するデコーダーを作成できるのです。

```elm
form : Decoder RegisterForm Error Goat
form =
    Decoder.top Goat
        |> Decoder.field name_
        |> Decoder.field age_
        |> Decoder.field horns_
        |> Decoder.field contact_
        |> Decoder.field memo_
```

なんと、**たったこれだけ**です。
ほんとにこれだけなんです。
各フィールド用のデコーダーを並べただけです。

指南書である Decoder を使うことで、このような簡潔な記述が可能になるのです。
あとはこの `form` を使って

```elm
Decoder.run form (RegisterForm "Sakura-chan" "2" "0" ...)
--> Ok (Goat "Sakura-chan" 2 0 ...)
```

のように `RegisterForm` から `Goat` への変換も可能になるのです。

以上、フォームデコーディングの紹介と elm-form-decoder による実例でした。
もし「elm-form-decoder おもしろいじゃん」と思ったら [Githubリポジトリ](https://github.com/arowM/elm-form-decoder) にスターをくださいm(_ _)m
また、elm-form-decoder自体の使い方について、詳しくて丁寧でわかりやすくて正確な説明を@miyamo_madoka さんが書いてくれました。
「[arowM/elm-form-decoderのAPIを【かんぜんりかい】しよう！](https://qiita.com/miyamo_madoka/items/d02f003ec1c212360111)」
こちらも合わせてご覧ください。


[さくらちゃんにご飯をあげる](https://www.amazon.co.jp/gp/registry/wishlist/20ZV2VC57615B/ref=nav_wishlist_lists_1)
[さくらちゃんをもっと見る](https://twitter.com/hashtag/%E3%81%95%E3%81%8F%E3%82%89%E3%81%A1%E3%82%83%E3%82%93%E6%97%A5%E8%A8%98?src=hash)
[他の記事を見る](https://qiita.com/arowM)
![P2019.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16345/0b631a39-39e7-299b-468c-e1fe17ca76ba.jpeg)


[^1]: ネイティブアプリ開発については詳しくないのでここでは触れていませんが、静的型付けを使っていれば話は同じはずです
[^2]: ただし、僕は感情を持ったひとりの人間なので「こんなのもう10年前からあるだろｗｗ情弱乙ｗ」などとコメントされると悲しい気持ちになり傷つきます。まっとうな人間として当然の最低限の配慮あるコメントをいただけると嬉しいです。
[^3]: もちろん諸説あると思いますが、本記事の本題ではないのでここでは「避けるべき設計」という前提でいったん読み進めてください。すごく何か言いたくて仕方ないのであればこの記事に長文コメントするよりも何かご自身で記事を書いていただくことが良識ある行動だと思います。