Advent Calendar参加してます
この記事はElm2 Advent Calendar 2019の19日目の記事です。
はじめに
こんにちは。
突然ですが、みなさんはElmのJSONの扱いで詰まったことはありませんか。僕はElmを触り始めた時、たった1ネストのJSONをデコードするのに丸1日かかったりして泣きそうになりました。
Elmはドキュメントが充実していますが、関数の型と1つexampleが載っている程度で、手取り足取り最後まで解説してくれているわけではありません。という訳で、今回は過去の自分のようなJSONのデコードが分からない人向けに、かなり親切目に解説記事を書いてみようと思います。全部REPLで試せるコードなので、ぜひ自分のターミナルで一緒に試してみることをおすすめします。
(こんな記事を読まなくても自分でREPLとドキュメントを読めばどう書けばいいのか分かるだろ!という意見は賛成ですが、少しでも理解の助けになれば幸いだと思って書いています。また、「こうも書けるよ!」「こう書いたほうが良いよ!」といった意見は歓迎です。)
想定する読者
Elm Guideを読み終わったけど自分でまだJSONのデコードがよく分かってない人
筆者について
Elmを触って1~2ヶ月ぐらいです。Elm楽しい。
きほんのき
まず、JsonをデコードするためのパッケージJson.Decode
をインポートする。
as D
とすることでそれ以降はD
として扱える。
import Json.Decode as D
Json.Decode.decodeString
を使う。
> D.decodeString
<function> : D.Decoder a -> String -> Result D.Error a
これは第1引数にDecoder
を受け取り、第2引数に(JSONの)文字列を受け取ることで第1引数のDecoder
が指定した型の値を返す関数。具体的には以下のようにして使う。
> D.decodeString D.string """ "a" """
Ok "a" : Result D.Error String
D.string
はString
用のDecoder
。
> D.string
<internals> : D.Decoder String
(実際にはdecodeValue
がjsとのやり取りでよく使われるようなのだけれど一旦後述)
Intの場合
↑のD.stringをD.intに変えるだけ。
> D.decodeString D.int """ "1" """
Ok 1 : Result D.Error Int
List (List String)
["a", "b"]
のようなリストのJSONをパースする。D.listを使う。
D.listはD.stringなどとは違って、
-
Decoder
を引数として受け取り、 - 返り値に引数の
Decoder
の型からなるリストのDecoder
を返す。
> D.list
<function> : D.Decoder a -> D.Decoder (List a)
なので、D.string
と組み合わせるとList String
のDecoder
ができる。
>D.list D.string
<internals> : D.Decoder (List String)
ここまでくれば、D.decodeString
の第1引数の型であるD.Decoder a
になっているので、組み合わせるとString
をList String
にパースできる関数が作れる。
> D.decodeString (D.list D.string)
<function> : String -> Result D.Error (List String)
なので、これにList StringのJSONを食わせてやれば
> D.decodeString (D.list D.string) """[ "a", "b"]"""
Ok ["a","b"] : Result D.Error (List String)
ElmのList Stringにデコードできる。yay!
複数の要素を1回で取り出すデコーダ
{i: Int, s: String}
を取り出せるデコーダを作る。
取り出したい要素の数に応じてmapN
を使う。
> D.map2
<function> : (a -> b -> value) -> D.Decoder a -> D.Decoder b -> D.Decoder value
これの型定義がややこしいのだけれど、要するに
- 第1引数:最終的に生成したい型のコンストラクタ(①)
- 第2引数:(①)の第1引数の型のデコーダ
- 第3引数:(①)の第2引数の型のデコーダ
という感じ。(mapNによって最終的に生成されるのはD.decodeString
の第1引数に与えるデコーダということを念頭に置いて考えましょう)
具体的には以下のような感じ。
まず、取り出したい要素の(カスタム)型を作る。
type alias MyRecord = { i : Int, s : String}
次に、map2を使ってデコーダを組み上げる。
> D.map2 MyRecord D.int D.string
<internals> : D.Decoder MyRecord
これが出来ればdecodeString
に渡せる。
myRecordDcoder = D.map2 MyRecord D.int D.string
> D.decodeString myRecordDcoder
<function> : String -> Result D.Error MyRecord
...と言いたいところなのだけれど、これでは成功しない。実際にJSONを渡すと、
> D.decodeString myRecordDcoder """ {"i": 5, "s":"two"} """
Err (Failure ("Expecting an INT") <internals>)
: Result D.Error MyRecord
と、エラーになってしまう。これはmyRecordDcoder
に問題があり、「JSONのどのkeyを取得するか」という情報が抜けているためである。というわけで書き直す。
keyの指定にはD.field
を使う。
> D.field
<function> : String -> D.Decoder a -> D.Decoder a
これは
- 取得したいkeyをStringで、
- valueの型の
Decoder
を受け取って、Decoder a
を返す関数。
> D.field """ "i" """ D.int
<internals> : D.Decoder Int
なので、map2
のD.int
たちをこれで書き換えてやれば良い。
> myRecordDecoder2 = D.map2 MyRecord (D.field """ "i" """ D.int) (D.field """ "s" """ D.string)
<internals> : D.Decoder MyRecord
で、decodeString
に渡す。
> D.decodeString myRecordDecoder2
<function> : String -> Result D.Error MyRecord
> D.decodeString myRecordDecoder2 """ {"i": 5, "s":"two"} """
Ok { i = 5, s = "two" }
: Result D.Error MyRecord
できました。yay!
ネストしたオブジェクト
上記の例から発展させて、ネストしたJSONをデコードしていく。具体的には以下のような形を想定する。
{ "i" : 1,
"s": "string",
"data" : {
"imageUrl" : "https://aws.s3..." }
}
まず追加された要素のtype aliasを作っていく。
> type alias Data = {imageUrl : String}
<function> : String -> Data
> type alias MyRecord = {i : Int , s: String, data : Data}
> MyRecord
<function> : Int -> String -> Data -> MyRecord
次に、Data
のデコーダを作る。
> dataDecoder =
D.map
Data
(D.field "imageUrl" D.string)
<internals> : D.Decoder Data
これを、↑で作ったmyRecordDecoder
に組合わせる。組み合わせる時は単純にfield
をネストさせれば良い。
> myRecordDecoder =
D.map3
MyRecord
(D.field "i" D.int)
(D.field "s" D.string)
(D.field "data" dataDecoder)
<internals> : D.Decoder MyRecord
動かしてみる。
> D.decodeString myRecordDecoder """ { "i" : 1, "s": "string", "data" : {"imageUrl" : "https://aws.s3..." }} """
Ok { data = { imageUrl = "https://aws.s3..." }, i = 1, s = "string" }
: Result D.Error MyRecord
無事にパースできました。やったぜ。
参考URL
もっと簡単なパース
NoRedInk/elm-json-decode-pipelineを使えばJSONから複数のレコードを取得するのがもっと簡単になる。