読者です 読者をやめる 読者になる 読者になる

uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかFregeとかJavaとか -

Elmでやってみるシリーズ15: Json.Decodeで9個以上のフィールドを持つobjectをデコードしてみる

elm json

本記事では、ElmのコアライブラリにおけるJSONデコードパッケージJson.Decodeを用いて、9個以上のフィールドをもつオブジェクトのデコード方法について説明する。

2015 08 31追記あり(andMapを使用する)

その前に

その前にJson.Decodeを簡単に説明する。 Json.Decodeは、JSONのデコード処理をするためのコアライブラリの一つである。Elm 0.14以前のものとは完全に置き換られている*1

Jsonデコード処理の概要

一般形式としては、

   decodeString : Decoder a -> String -> Result String a
   decodeString <デコーダ> <JSON文字列> = ...

である。decodeStringは、第二引数<JSON文字列>をJSON文字列としてパースし、<デコーダ>で指定した型のデコード結果を返却する。デコーダは1つの型パラメタaをとる多相型(Decoder a)の値であり、decodeStringにDecoder a型のデコーダを与えて呼び出したとき、パースが成功していたら、a型の結果がOk aとして返る。型の不一致やパース失敗のときはErr Stringが返る(Resultの型は、type Result error value = Ok value | Err error)。

Json.Decodeにおいて、JSON文字列からのパース処理は、基本的にdecodeStringから始まる。

Jsonデコード処理の例

以下はJsonデコード処理の例である。

import Json.Decode (..)
import Text (..)
import Graphics.Element (flow,down)
import List

main = flow down 
       [ asText <| decodeString string "\"abc\"" --(1)
        , asText <| decodeString int "3" --(2)
        , asText <| decodeString (list string) "[\"abc\",\"def\"]" --(3)
        , asText <| decodeString (list bool) "[true,false,true]" --(4)
        , asText <| decodeString (array bool) "[true,false,true]" --(5)
        , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]" --(6)
        , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}" --(7)
        , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (8)
        , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9)
        , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }" --(10)
        , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)] --(11)
       ]
  • (1),(2)におけるstringintなどはデコーダ型の値である。
  • (3),(4),(5)におけるlist, arrayなどはデコーダ型の値をとってデコーダを返す関数である。いずれもJson.Decodeで定義されている。list、arrayはいずれもJSONの配列からデコードするデコーダであるが、listは結果としてList型を、arrayはArray型の値を取り出す(デコードする)。このように、デコーダは「取り出したい型、結果として得たい型」を与えるものである。JSONの配列の要素の型は一致している必要がある。
  • (6)のtuple3は、3要素のJSONの配列と、それぞれの配列要素を引数としてとる3引数の関数を引数として与えると、その関数の結果をデコード型として得ることができる。「touple*」という名称だが、デコード結果として得ることができるのはタプル型に限らない。拡張レコード型だろうがリストだろうが別の関数を呼び出した何かの処理結果であろうが、なんでも御座れである。ただしたとえば返したい型がElmリストの場合は、デコード対象のJSONの配列のすべての要素の型が一致していないとデコードに失敗するであろう。
  • (7)の(a := int)は、オブジェクトのフィールドa(Int型)をデコードするデコーダである。該当するフィールドが存在しなければErrが結果として得られる(8)
  • (9)複数のフィールドを同時にデコードするにはobject<N>(Nは1〜8)を用いる。ここではint型フィールドaとString型フィールドbをデコードし、結果としてタプル(Int,String)の値を返している。
  • (10)atは、指定したフィールドaがオブジェクト型である場合、そのオブジェクトのフィールドbを取得して…のようにネストしたオブジェクトを順に辿っていくためのデコーダである。

9個以上のフィールドのデコードの問題

さて、オブジェクトをデコードする場合、上記(9)で使用したように、object<N>という関数を使用すると良い。問題は、Nが8までのしか定義されていないことである。少なっ!

9個以上のフィールドをデコードするにはどうしたら良いであろうか。一つの案は、ちまちまと:=を用いて、1個ずつフィールドを取り出せばよい。あるいは(11)のように何個かの:=に対してList.mapで一気にdecodeStringの結果を得ることもできる。しかし、decodeStringがパース処理の呼び出しそのものであり、効率が悪いという問題がある*2。さらに、結果が、Resultの配列なのも気になる。どれか1個がErrなら、全体がErrになってほしい。

この問題を解決する方法を以下に示す。

9個以上のフィールドをデコードする方法

まず、JSONのデコード結果として以下のような拡張レコードで結果を得たいものとする。タプルでも良いのだが、とりあえず。

type alias MyData = {
    f1 : Maybe String
  , f2 : Maybe Int
  , f3 : Maybe Float
  , f4 : Maybe String
  , f5 : Maybe String
  , f6 : Maybe String
  , f7 : Maybe String
  , f8 : Maybe String
  , f9 : Maybe String } -- 9個以上

そして、以下のようにヘルパ関数filedを準備する。

field : Decoder a -> String -> Dict.Dict String Value -> Maybe a
field typ fld dic = case (case Dict.get fld dic of -- (*)
                            Just val -> decodeValue (maybe typ) val
                            Nothing -> Err fld) of
                      Ok res -> res
                      Err _ -> Nothing

ここで(*)での「Dict.get fld dic」で得られるのは、Json.Decode.Value型の値で、そこから実際の値をとりだすためにdecodeValueを使用する。ここでのdicは後述の「dict value」デコーダを用いることで得られる、「JSONオブジェクトのデコード結果としてのDictの値」である。あとはちまちまとResultやMaybeでラッピングされた値をパターンマッチングでほどいて、最後にまたMaybeでラッピングしている。

このヘルパ関数を以下のように使用して、MyDataをデコードするデコーダを定義する。

myDataDecoder : Decoder MyData
myDataDecoder = customDecoder (dict value) (\dic ->
                                     Ok {
                                            f1 = field string "f1" dic
                                          , f2 = field int "f2" dic
                                          , f3 = field float "f3" dic
                                          , f4 = field string "f4" dic
                                          , f5 = field string "f5" dic
                                          , f6 = field string "f6" dic
                                          , f7 = field string "f7" dic
                                          , f8 = field string "f8" dic
                                          , f9 = field string "f9" dic
                                     })

deocdeStringに、デコーダdict value」を与えることが、肝心要の要点である。得られるのはキーがString、バリューがDict Json.Decode.Value型のDict(辞書)型の値のデコーダである。ちなみに、Json.Decode.Valueは、JSONのプリミティブや配列やObjectなどを直和したADTではなく、Valueという型引数も持たない、単一のデータコンストラクタだけを持つ型である。そんな型が何の役にたつかというと、これは実はNativeのJavaScriptの中だけで意味を持つものであり、Java Script側ではJSONのノードを保持している。Json.Decode.Valueは、本来その存在を気にする必要はないようにAPIが設計されているのだが、今回のように9個以上のフィールドを一気にデコードする、とかこういうときにはおおいに気にすることになるわけである。

上記デコーダmyDataDecoderの使い方は以下のとおり。

        decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"

全体のコードはShare-elm上にも置いたが、以下のとおり。

import Json.Decode (..)
import Text (..)
import Graphics.Element (flow,down)
import List
import Dict

type alias MyData = {
    f1 : Maybe String
  , f2 : Maybe Int
  , f3 : Maybe Float
  , f4 : Maybe String
  , f5 : Maybe String
  , f6 : Maybe String
  , f7 : Maybe String
  , f8 : Maybe String
  , f9 : Maybe String } -- 9個以上

field : Decoder a -> String -> Dict.Dict String Value -> Maybe a
field typ fld dic = case (case Dict.get fld dic of
                            Just val -> decodeValue (maybe typ) val
                            Nothing -> Err fld) of
                      Ok res -> res
                      Err _ -> Nothing

myDataDecoder : Decoder MyData
myDataDecoder = customDecoder (dict value) (\dic ->
                                     Ok {
                                            f1 = field string "f1" dic
                                          , f2 = field int "f2" dic
                                          , f3 = field float "f3" dic
                                          , f4 = field string "f4" dic
                                          , f5 = field string "f5" dic
                                          , f6 = field string "f6" dic
                                          , f7 = field string "f7" dic
                                          , f8 = field string "f8" dic
                                          , f9 = field string "f9" dic
                                     })

main = flow down 
       [ asText <| decodeString string "\"abc\""
        , asText <| decodeString int "3"
        , asText <| decodeString (list string) "[\"abc\",\"def\"]"
        , asText <| decodeString (list bool) "[true,false,true]"
        , asText <| decodeString (array bool) "[true,false,true]"
        , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]"
        , asText <| decodeString (tuple3 (\a c b->(a,b,c)) bool int string) "[true, 1, \"abc\"]"
        , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}"
        , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (9)
        , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9)
        , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)]
        , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }"
        , asText <| decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"
        , asText <| decodeString myDataDecoder "[]" == Err ("expecting an object but got []")
       ]

実行結果は以下のようになる。

Ok "abc"
Ok 3
Ok ["abc","def"]
Ok [True,False,True]
Ok (Array.fromList [True,False,True])
Ok (True,1,"abc")
Ok (True,"abc",1)
Ok 1
True
Ok (1,"foo")
[Ok 1,Ok 2]
Ok 2
Ok { f1 = Just "abc", f2 = Just 3, f3 = Just 3.3, f4 = Just "s", f5 = Nothing, f6 = Just "s", f7 = Just "s", f8 = Just "s", f9 = Just "s" }
True

他にも良いやりかたがあったらおしえてちょう!(NativeでやるとかPortでやるのは無しとして)

まとめ

decodeStringは内部でまずはJSONをパースして、いったんかならずJson.Decode.Valueのツリーが形成されているはず。なので、そこに対してデコーダをかませていく処理は、「ツリーをパースするVisitorパターン」でありDecoderがビジターである。そしてそのVisitor=Decoderは、パーサコンビネータライブラリのように合成していく。わかってしまえばなんということはない。 ない、のだが、動的言語とかでJson扱うのとは別次元の面倒さが発生していることは否定できないなー。

しかし、8は少ないだろ、8は…。

(追記) プルリクを打ってみた: object decoder for more than 8 fields · uehaj/core@3d49629 · GitHub

(追記2)

メーリングリストで以下のやりかたを教えてもらった。

myDataDecoder =
         ("f1":=int) `andThen` (\x1 ->
         ("f2":=string) `andThen` (\x2 ->
         ("f3":=float) `andThen` (\x3 ->
         ("f4":=string) `andThen` (\x4 ->
         ("f5":=maybe string) `andThen` (\x5 ->
         ("f6":=string) `andThen` (\x6 ->
         ("f7":=string) `andThen` (\x7 ->
         ("f8":=string) `andThen` (\x8 ->
         ("f9":=string) `andThen` (\x9 -> succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9})))))))))

Decoder.andThenはモナドのbind(>>=)です。なるほどねー。 でもこれはこれで見にくいし書きにくい。 do記法があれば

myDataDecoder = do
         x1 <- ("f1":=int)
         x2 <- ("f2":=string)
         x3 <- ("f3":=float)
         x4 <- ("f4":=string)
         x5 <- ("f5":=maybe string)
         x6 <- ("f6":=string)
         x7 <- ("f7":=string)
         x8 <- ("f8":=string)
         x9 <- ("f9":=string)
         succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9}

こう行ってたとこなんだけどね。 あるいは<~と~(Haskellの<$><*>)を定義して、

myDataDecoder = (\x1 x2 x3 x4 x5 x6 x7 x8 x9 -> {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9})
                             <~ ("f1":=int)
                               ~ ("f2":=string)
                               ~ ("f3":=float)
                               ~ ("f4":=string)
                               ~ ("f5":=maybe string)
                               ~ ("f6":=string)
                               ~ ("f7":=string)
                               ~ ("f8":=string)
                               ~ ("f9":=string)

こう。

andMapを使う方法(追記2015/08/31)

以下で行けます。これが今のところベストなやりかたと思う。 参考: https://groups.google.com/d/msg/elm-discuss/J9ip8MqXtYM/u4aRGAlbSkcJ

import Json.Decode as Decode
import Json.Decode exposing (..)
import Text exposing (fromString)
import Graphics.Element exposing (flow,down,show)
import List

type alias MyData = {
    f1 : String
  , f2 : Int
  , f3 : Float
  , f4 : String
  , f5 : Maybe String
  , f6 : String
  , f7 : String
  , f8 : String
  , f9 : String }

andMap : Decoder (a -> b) -> Decoder a -> Decoder b
andMap = Json.Decode.object2 (<|)

myDataDecoder2 : Decoder MyData
myDataDecoder2 =
    MyData `Decode.map` ("f1":=Decode.string)
           `andMap` ("f2":=Decode.int)
           `andMap` ("f3":=Decode.float)
           `andMap` ("f4":=Decode.string)
           `andMap` ("f5":=Decode.maybe Decode.string)
           `andMap` ("f6":=Decode.string)
           `andMap` ("f7":=Decode.string)
           `andMap` ("f8":=Decode.string)
           `andMap` ("f9":=Decode.string)

main = flow down
       [ show <| decodeString myDataDecoder2 "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"
       ]

リンク

*1:以前のものの使用例はこちらでも紹介しているが、無用のものとなった

*2:本当に何度も呼びだすかは未確認。でもたぶん呼んでる。Elmにおいて、引数が同じ文字列で、純粋関数だからメモ化される、という期待はできない。さらにもちろんElmは遅延評価ではなくサンクなんかない。Signalにliftされた関数からsignal値としてのJSONが引数に与えられているなら、JSON文字列の更新がなければ多数回呼び出されないと考えてよいが、ここで考えている例の場合はあてはまらない。