関数型よく知らない人のelm入門(5) - Jsonパース実践サンプル集
フロントエンドのコードを作成するとき、誰もが遭遇するであろう Jsonパース について今回はとりあげたいと思います。
これがなれるまでつらい。
Java等の言語が「Jsonオブジェクトから要素をとりだしていく」のに対し、elmでは「パースロジックを組み上げいく」というフローで実態が見えにくく、理解を困難にしています。
特に「途中経過をログ出力させる」のが難しい(面倒)。パースをミスしたときのエラー原因も分かりづらいです。
そこで今回はまずは組めるようになるという方針でいきます。
題して、JSONパース実践サンプル集
細かい理論は後回し、習うより慣れろ、理解はできた後にやってくるのコンセプトの元、とにかくサンプルコードを書いてみます。
ぜひオンラインエディタで色々編集しながら試してください。
(1)単純な型
公式サンプルにもあるのであまり必要はないと思いますが一応。
{ "id": 10, "name": "Mike" }
import Html exposing (text) import Json.Decode as Decode exposing (..) jsonText = "{\"id\" : 10,\"name\" : \"Foo\"}" type alias User = { id: Int, name : String } decodeUser : Decoder User decodeUser = Decode.map2 User (field "id" int) (field "name" string) main = let user = decodeString decodeUser jsonText in case user of Ok u -> text (toString u) _ -> text "Error!"
Decode.map2
がミソです。 User はメンバー2つから生成されるので、map2で引数を順番に2つ指定します。 type alaisの宣言順にdecodeも記述する必要がある ので注意してください。
それ以降は
- “id” というキーのValueを int として解釈し第一引数に
- “name” というキーのValueを String と解釈し第二引数に
- map2 でその二つから User を生成
という流れです。最初はわけわからんですよね….慣れれば簡単なのですが…
まずはこのパターンをいくつか練習するのをオススメします。
※これ以降、importや後半の表示部分は一緒なので省略します。
(2)Optional Value
「値がないかもしれない」というパターンですね。かなりあるとパターンですが最初はどうすればいいのかさっぱりでした…分かれば簡単。
{ "id" : 12345, "icon_url": "http://..." ↑ 場合によっては icon_url がないとする }
値がないかもしれないので、もちろん型定義では Maybe
を使います。
jsonText1 = "{\"id\" : 1234, \"icon_url\": \"http://...\"}" jsonText2 = "{\"id\" : 6789}" -- 値がないかもしれないので、iconUrlは Maybe type alias User = { id: Int , iconUrl : Maybe String } decodeUser : Decoder User decodeUser = Decode.map2 User (field "id" int) (maybe (field "icon_url" string)) -- ある場合 -- {id = 1, iconUrl = Just "http://..."} decodeString decodeUser jsonText1 --ない場合 decodeString decodeUser jsonText2 -- {id = 1, iconUrl = Nothing }
Decode.maybe
を先頭につけるだけです。 ()の位置 に気を付けてください。付け忘れると動きません。
(3)値がないときは空文字にしたい
これもよくあるパターンですね。値がなかったらデフォルト値にしたい。
一つ上の Maybeパターンで、 もしicon_urlがなかったら空文字 というパターンにしてみましょう。
jsonText自体は一緒、ただし型定義が変わります。今回は「必ず値があるはず」なのでMaybeではありません。
type alias User = { id: Int , iconUrl : String } decodeUser : Decoder User decodeUser = let id = (field "id" int) iconUrlRaw = (field "icon_url" string) iconUrlMaybe = Decode.maybe iconUrlRaw iconUrl = Decode.map (Maybe.withDefault "") iconUrlMaybe in Decode.map2 User id iconUrl
ちょっと長くなるので一旦 let で変数に格納しました。
基本はMaybeですが、 Maybe.withDefaul と Decode.map を使って、値がないときの変換処理を書いてます
(withDefaultは値がなかった場合はデフォルトを返す関数です。 Swiftでいう、 v = optValue ?? ""
)
もちょっと楽な方法がある気はするのですが…とりあえずこれで出来ます。
(4)定義した型の配列
次に単純な数値/文字列の配列でなく、自分で定義した型の配列が返ってくる場合です。こちらもAPIアクセスではよくあると思います。
型自体は (1) の { id,name } パターンを使います
[ { "id" : 1, "name" : "Tom" }, { "id" : 2, "name" : "Mike" } ]
jsonText = "[" ++ "{\"id\" : 1,\"name\" : \"Tom\"}," ++ "{\"id\" : 2,\"name\" : \"Mike\"}" ++ "]" type alias User = 省略 decodeUser = 一緒なので省略 --これだけ decodeUserList : Decoder (List User) decodeUserList = Decode.list decodeUser --パースできる decodeString decodeUserList jsonText
簡単ですね、慣れれば一瞬です。
(5)特定のキー名でUser配列がある
さぁどんどん複雑になっていきますよ。これも頻出パターン
(4)のパターンに細工を加えた、こんな感じのJsonです
{ "user_list" : [ { "id" : 1, "name" : "Tom" }, { "id" : 2, "name" : "Mike" } ] }
単に今までのを組み合わせるだけで出来ちゃいます
jsonText = "{ \"user_list\" : [" ++ "{\"id\" : 1,\"name\" : \"Tom\"}," ++ "{\"id\" : 2,\"name\" : \"Mike\"}" ++ "] }" type alias User = 略 decodeUser = 略 decodeUserList : Decoder (List User) decodeUserList = Decode.field "user_list" (Decode.list decodeUser)
ようは、user_list という要素をとりだして、(4)と同じものに突っ込んでるだけですね。
慣れればすぐできるようになります。
(6)2階層深い部分を一気にとりだす
さらに複雑化させましょう
{ "owner" : { "follower" : [ { "id" : 1, "name" : "Tom" }, { "id" : 2, "name" : "Mike" } ] } }
先ほどと同じUser配列が、owner-follower という要素にあります。
実はこれは一発でとりだせます
jsonText = "{\"owner\" : {" ++ "\"follower\" : [" ++ "{\"id\" : 1, \"name\" : \"Tom\"}," ++ "{\"id\" : 2, \"name\" : \"Mike\"}" ++ "]}}" --user定義等は省略 decodeUserList : Decoder (List User) decodeUserList = Decode.at ["owner", "follower"] (Decode.list decodeUser)
field
を使う変わりに、 at
で階層を指定するだけです。
ここまでできれば大半のパース処理は行えるのではないかと思います。
(7)配列の先頭のみ返す
上記Userで、配列の先頭のみ抽出して返すパターンを考えてみましょう。
配列の要素が 0個 の場合はエラー扱いにします。
//tomのみ欲しいとする [ { "id" : 1, "name" : "Tom" }, { "id" : 2, "name" : "Mike" } ]
jsonText1 = "[" ++ "{\"id\" : 1,\"name\" : \"Tom\"}," ++ "{\"id\" : 2,\"name\" : \"Mike\"}" ++ "]" jsonText2 = "[]" --空配列 type alias User = 略 decodeUser = 略 --一緒ですけど一応 decodeUserList : Decoder (List User) decodeUserList = Decode.list decodeUser --今回のメイン decodeFirstUser : Decoder User decodeFirstUser = decodeUserList |> Decode.andThen (\userList -> case (List.head userList) of --先頭をとりだしてそのまま結果にする Just r -> Decode.succeed r --0件のときはエラー文つきで失敗にする Nothing -> Decode.fail "Empty List" ) main = let r1 = decodeString decodeFirstUser jsonText1 r1 = decodeString decodeFirstUser jsonText2 in div [] [ case r1 of Ok u -> text (toString u) Err e -> text e , case r ] decodeFirstUser : Decoder User decodeFirstUser = decodeUserList |> Decode.andThen (\userList -> case (List.head userList) of Just r -> Decode.succeed r Nothing -> Decode.fail "Empty List" ) -- 正常。結果がでる decodeStrind decodeFirstUser jsonText1 -- 空配列。以下のようなエラーになる -- I ran into a `fail` decoder: Empty List decodeStrind decodeFirstUser jsonText12
andThen でつなげるのがミソ。これで「Jsonパース自体は空配列として成功してるけど、要素がないから手動で失敗扱いにする」ことができます。
ただこれはパースした結果のListをみて処理を書いてもそう大きな差はないと思います。(6)までと違い、パース自体はできているのですから。
むしろ結果から処理したほうが分かりやすいかもしれませんね。
(8)要素が9個以上の型のパース
実は地味に厄介なのがコレ。 Decode.map8
で要素8つの型にまでは対応できるのですが、それ以上は対応できません。
そこで登場するのが、decode-pipelineライブラリ
上のgithubリンクを見れば分かりますが、要素数が8以下でも直感的に記述できて非常に便利です。
decodeUser : Decoder User decodeUser = Decode.succeed User |> required "id" int |> required "name" string decodeUser2 = Decode.succeed User2 |> required "id" int |> optional "icon_url" string
必須の型は required
、Maybeは optional
でパースできます。非常に楽ですね。
ただ、実はライブラリとしては小さい。ソースを見れば分かりますがほとんどがコメントです。
なので、 これは自作できるようになったほうがいいです。
まったく同じ関数でなくてもいいので、同じようなことを・9つ以上のパースをどうするか一度考えてコードを書いてみてください。
もちろんこのライブラリのソースを参考にしても問題ありません(理解せずコピペはNG)。同じように要素9つのパースができたころには、Jsonパースは楽に組めるようになっているでしょう。
そして、このライブラリのコアである次の関数も理解できるようになるのではないかと思います。
custom : Decoder a -> Decoder (a -> b) -> Decoder b custom = Decode.map2 (|>)
自分は頭が悪いようで、これを理解するのに数日かかりました…しかし、まだ完全に理解しきったとは言えないでしょう。
これ初見で分かる人はそうとうすごい…
と、よくあるのではないかと思うパターンを挙げてみました。
サンプルのJson文字列も elmのオンラインエディタ で試せるよう、ちゃんとエスケープして貼ってあります。
ぜひ色々試して、理解を深めてください。
なれたころには元の言語のJsonObjectを使ったパースに違和感を覚えるかもしれません。