この記事はFringe81 Advent Calendar 1日目の記事です。
Elmでアプリケーション開発をしているとほぼ100%出現するのがエンコーダ・デコーダの実装。
基本的に両方セットで実装することが多いが、データ構造が共通であればCodecと呼ばれる類の機能を提供するライブラリを用いてエンコーダ・デコーダの実装をケチることができる。
Codecとは
Codecとは簡単に言うとデータ構造が共通なものであればひとつCodec用の関数を定義しておくだけで、エンコーダとデコーダを同時に実装することができる便利なもの。
まず、Elmでは書き込みと読み込みでエンコーダとデコーダが別れているため、基本的に同じデータ構造に対してそれぞれ実装する工数があるかつ実装が別々であるため改修時に実装ミスが起きるなどの可能性がある。
しかしCodecを利用することでエンコーダとデコーダをそれぞれ実装する必要がなくなる上、デコーダやエンコーダにだけtypoがあった!みたいなバグを減らすことができる。
Codec的なパッケージとしていま利用できるものにはminiBill/elm-codecとMartinSStewart/elm-serializeのふたつがある。この記事ではそれぞれの違いや使い分けたほうがよさそうなポイントを紹介する。
elm-codec
package.elm-lang.org
最も最初にCodecという概念を誕生させたのはこのライブラリ。
兎にも角にもまずはサンプルコードから。以下のようなレコードに対してCodecを実装してみる。
type Language
= Japanese
| English
| Other String
type alias Model =
{ name : String
, age : Int
, lang : Language
}
Codecの実装はこんな感じになる。
modelCodec : Codec.Codec Model
modelCodec =
Codec.object Model
|> Codec.field "name" .name Codec.string
|> Codec.field "age" .age Codec.int
|> Codec.field "lang" .lang languageCodec
|> Codec.buildObject
languageCodec : Codec.Codec Language
languageCodec =
Codec.custom
(\jp en other value ->
case value of
Japanese ->
jp
English ->
en
Other v ->
other v
)
|> Codec.variant0 "japanese" Japanese
|> Codec.variant0 "english" English
|> Codec.variant1 "other" Other Codec.string
|> Codec.buildCustom
上記のようにまずレコードに対するCodecを実装した上で、そこに含まれるカスタム型のCodecも実装できる。
基本的に入れ子構造にしてデータ構造を用意でき、使っている感じはデコーダの実装に近いと思われる。パイプラインでバシバシつなげるのもいい感じ。
あとは用意されている関数を用いてValue型にエンコード・デコードできる。使い方は基本これだけ。
Codec.encodeValue modelCodec model
Codec.decodeValue modelCodec value
ドキュメントを見てもらえればわかるがstring, bool, intなどの基本的な関数は用意されており、ResultやDict, Setなどにも対応している。また、カスタム型も最大8バリアントまで実装されている。
さて、ここまでくると一体どういうデータ構造のJSONがエンコードされるのかが気になってくる。データ構造上はエンコーダとデコーダが共通でも動くものになるはずだ。
というわけで、上記のコードで示されているModelレコードをエンコードすると以下のようなJSONが作成される。
{
"lang":{
"tag":"other",
"args":[
"unknown"
]
},
"age":10,
"name":"seiya"
}
age
や name
は想像通りだが、カスタム型として定義されている lang
には若干クセがある。
tag部分にはelm-codecが提供する Codec.variantN
関数に渡されているフィールド名が入り、そしてもしそのバリアントがタグとなるデータを持っている場合にはそれを args
というフィールドで保持するという作りになっている。たしかに、こうでもしないとカスタム型はそのままJSONに格納できない。
elm-codecでは上記のようなデータ構造のみがカスタム型に対してマッピングできる作りになっているため、結論JSONのデータ構造がelm-codecによって制限されることになる。エンコーダ・デコーダを個別に定義しなくてよくなるのは楽ではあるが、JSONのデータ構造の柔軟性を失うのは若干痛い。
elm-serialize
package.elm-lang.org
elm-serializeはelm-codecよりも後発のパッケージであり、elm-codecと比較してパフォーマンスや安全性に関する点で違いがある。
パッケージ自体の使い心地はelm-codecとさほど変わらない。以下が実装例となる。Modelレコードはelm-codecの例で実装されているものと同じものとする。
modelSerializer : Serialize.Codec a Model
modelSerializer =
Serialize.record Model
|> Serialize.field .name Serialize.string
|> Serialize.field .age Serialize.int
|> Serialize.field .lang languageSerializer
|> Serialize.finishRecord
languageSerializer : Serialize.Codec a Language
languageSerializer =
Serialize.customType
(\jp en other value ->
case value of
Japanese ->
jp
English ->
en
Other v ->
other v
)
|> Serialize.variant0 Japanese
|> Serialize.variant0 English
|> Serialize.variant1 Other Serialize.string
|> Serialize.finishCustomType
定義した雰囲気もほぼほぼ同じだが、まずひとつelm-codecとの違いとしてelm-serializeはValue型だけではなくelm/bytesへの変換もサポートされている。対象となるデータ構造のサイズに応じてValue型とBytes型を使い分けることができる。
最も異なるのは生成されるJSONの構造で、こちらはelm-codecよりももっとクセがある。
エンコードすると以下のようなデータになる。
[
1,
[
"seiya",
10,
[
2,
"unknown"
]
]
]
elm-serializeはREADMEで「human-readableなフォーマットを求める場合には適さない」と書かれており、生成されるJSONの可読性を犠牲にしてJSONのサイズが小さくなるように工夫されており、そのためこのような形のデータ構造となっている。
また、elm-serializeはパッケージ内で定義されるエンコード・バージョンを生成されるJSONのデータに埋め込んでいる。上記のJSONで言うと配列トップレベルの 1
という数値がそれにあたる。この値はelm-serializeの中で定義されているものになっている。
type Error e
= CustomError e
| DataCorrupted
| SerializerOutOfDate
version : Int
version =
1
https://github.com/MartinSStewart/elm-serialize/blob/1.2.1/src/Serialize.elm#L93-L112
JSONにエンコード・バージョンを付与することで、仮に今後elm-serialize自体にbreaking changesがありエンコードされるJSONのデータ構造が変わってデコードエラーが発生したとしても、それがelm-serializeのバージョンアップ起因なのかそれとも純粋にデコード対象のJSONのデータ構造が間違っているのかを型レベルで判別できるようになっている。
このような後方互換性の考慮はelm-codecにはないため、この点はelm-serializeのほうが優れていると言える。
まとめ
「エンコードとデコーダをそれぞれ定義しなくていいなんて最高だな」と思って触っては見たものの、どちらもある程度のクセはあるため、どういう用途でCodecを使うかによっておそらく使い分けが必要になると思われる。
生成されるJSONのデータ構造をElmアプリケーションの外で触るようであればデータの構造は非常に大事であるのでelm-codecを使うべきだが、LocalStorageに保存するデータなどデータ・サイズに制限があるような環境においてはelm-serializeのほうが適していると言える。
ちなみにだがelm-firestoreにもCodecが実装されていて、データベースやLocalStorageに対するCRUDのようなデータソースの構造が一意に決まるシステムの入出力部分にはCodecのような仕組みを利用するのが適しているということが分かる。このような観点では、常に一意に入出力のデータ構造が決まるとは言えないサーバーサイドとの通信部分でエンコーダ・デコーダが必要になるWebフロントエンドのアプリケーションにおいてはCodecはあまり登場することはないのかもしれない。