mike-neckのブログ

Java or Groovy or Swift or Golang

HaskellでJSONのパーサーを書いてみた

POSTDというサイトに次のようなエントリーがあった。

postd.cc

で、この記事の中でParsecライブラリーの紹介としてJSONパーサーを作るという動画が紹介されていた。

www.youtube.com

Haskellをまだ始めたばかりなので、練習になるかなと思って、動画を見ながらJSONパーサーを書いてみた。


以下、上記の動画の再現

1.まず文字列"true"にマッチングするパーサーを作る

import Text.ParserCombinators.Parsec hiding((<|>, many)
import Control.Applicative
import Control.Monad

matchTrue:: Parser String
matchTrue = string "true"

この関数を文字列に対して適用すると文字列が"true"だった場合はRightが返され、それ以外の文字列の場合はLeftが返される。

ghci > parse matchTrue "test" "true"
Right "true"
ghci > parse matchTrue "test" "foo"
Left "test" (line 1, column 1):
unexpected "f"
expecting "true"

2.常時Trueを返すパーサーを作る

alwaysTrue:: Parser Bool
alwaysTrue = pure True

これは常にParser BoolBoolTrueのものを返す関数で、これ単独では特に使うことはない。

3.trueの場合にTrueを返すパーサーを作る

2.の常時Trueを表すパーサーをどのように用いるかというと、*>オペレーターを使ってマッピングする。

boolTrue:: Parser Bool
boolTrue = matchTrue *> alwaysTrue

これを文字列に適用すると、trueの場合にはRight True、それ以外の場合はLeftが返される。

ghci > parse boolTrue "test" "true"
Right True
ghci > parse boolTrue "test" "foo"
Left "test" (line 1, column 1):
unexpected "f"
expecting "true"

4.同様にfalseの場合にFalseを返すパーサーを作る

1〜3の要領でfalseに対するパーサーを作る

-- jsonのfalseをマッチング
matchFalse:: Parser String
matchFalse = string "false"

-- HaskellのFalseを取得
alwaysFalse:: Parser Bool
alwaysFalse = pure False

-- jsonのfalseをHaskellのFalseに変換
boolFalse:: Parser Bool
boolFalse = matchFalse *> alwaysFalse

なお、演算子(*>)は左側の値を捨てて、右側の値にするというものらしい(ドキュメントを斜め読みした)。

(*>) :: Applicative f => f a -> f b -> f b

boolFalseを文字列に適用した時、与えた文字列が"false"であればRight False、それ以外の場合はLeftが帰ってくる。

ghci > parse boolFalse "test" "false"
Right False
ghci > parse boolFalse "test" "true"
Left "test" (line 1, column 1):
unexpected "t"
expecting "false"

5.booleanに対応するパーサーの作成

trueが入っているか、falseが入っているか知ったこっちゃないので、booleanに対応するパーサーを作成する

bool:: Parser Bool
bool = boolTrue <|> boolFalse

ここで演算子(<|>)は次のような演算子Alternativeという名前から推測できるように、「〜あるいは〜」のような演算子と思われる(ちゃんと調べてない)。

(<|>) :: Alternative f => f a -> f a -> f a

この関数を文字列に対して適用すると、次のようになる。

ghci > parse bool "test" "false"
Right False
ghci > parse bool "test" "true"
Right True
ghci > parse bool "test" "foo"
Left "test" (line 1, column 1):
unexpected "o"
expecting "false"

6.stringに対応するパーサー

stringに対応するパーサーを作成する。文字列は"で前後が囲まれているので、これらを捨てる必要がある。

stringLiteral:: Parser String
stringLiteral = char '"' *> (many (noneOf ['"'])) <* char '"'

これを文字列に適用してみると、このようになる。

ghci > parse stringLiteral "test" "\"test\""
Right "test"
ghci > parse stringLiteral "test" "\"foo"
Left "test" (line 1, column 5):
unexpected end of input
expecting "\""

7.一旦JSONデータ型を作る

これまでに対応したパーサーに対応するJSONのデータ型を作る。

data JsonValue = JsonBool Bool | JsonString String deriving (Show)

JsonBoolを生成するパーサーを作る。

jsonBool:: Parser JsonValue
jsonBool = JsonBool <$> bool

同様にJsonStringを生成するパーサーを作る。

jsonString:: Parser JsonValue
jsonString = JsonBool <$> stringLiteral

ここで、演算子(<$>)Functorfmapみたいなもの(といういい加減な理解)。

(<$>) :: Functor f => (a -> b) -> f a -> f b

stringがくるか、booleanがくるか知ったこっちゃないので、何(stringまたはbooleanのいずれか)が来ても対応できるパーサーを作る。

jsonValue = jsonBool <|> jsonString

これによって、stringあるいはbooleanをパースできて、かつ独自のデータ型を持つことができるようになる。

ghci > parse jsonValue "test" "true"
Right (JsonBool True)
ghci > parse jsonValue "test" "\"test\""
Right (JsonString "test")
ghci > parse jsonValue "test" "foo"
Left "test" (line 1, column 1):
unexpected "o"
expecting "false"

8.arrayに対応する

arrayは要素の前後にある[]を捨てて、中身は,で区切られるので、次のようになる。

array:: Parser [JsonValue]
array =
    char '[' *>
    (jsonValue `sepBy` (char ',')) <*
    char ']'

また、JsonValueデータ型にarrayに対応するデータコンストラクターを追加する

data JsonValue =
    JsonBool Bool
    | JsonString String
    | JsonArray [JsonValue]
    deriveing (Show)

そして、上記のパーサーからJsonValue型に変換する関数を追加する

jsonArray:: Parser JsonValue
jsonArray = JsonArray <$> array

また、汎用的なパーサー関数jsonValueにarrayのものを加える

jsonValue = jsonBool
    <|> jsonString
    <|> jsonArray

これにより、arrayに対応するJSONパーサーが出来上がる。

ghci > parse jsonValue "test" "[]"
Right (JsonArray [])
ghci > parse jsonValue "test" "[true,false,\"test\"]"
Right (JsonArray [JsonBool True,JsonBool False,JsonString "test"])
ghci > parse jsonValue "test" "[foo]"
Left "test" (line 1, column 2):
unexpected "o"
expecting "false"

9.キー・バリューに対応する

キーと値のペアに対応するパーサーを作成して、JSONオブジェクトをパースできるようにする。

JSONオブジェクトは:で区切られていることに注意すると次のようになる。

objectEntry:: Parser (String, JsonValue)
objectEntry = do
    key <- stringLiteral
    char ':'
    value <- jsonValue
    return (key, value)

JsonValue型にオブジェクト用のデータコンストラクターを追加する

data JsonValue =
    JsonBool Bool
    | JsonString String
    | JsonArray [JsonValue]
    | JsonObject [(String, JsonValue)]
    deriving (Show)

JsonValue型へ変換するパーサーを作成する。

jsonObject:: Parser JsonValue
jsonObject =
    JsonObject <$>
        ((char '{') *> 
        (objectEntry `sepBy` (char ',')) <*
        (char '}'))

これを汎用のパーサーに追加する。

jsonValue = jsonBool
    <|> jsonString
    <|> jsonArray
    <|> jsonObject

以上により、numberを除くすべての要素に対応するJSONパーサーができた。

ghci > parse jsonValue "test" "{}"
Right (JsonObject [])
ghci > parse jsonValue "test" "{\"success\":true,\"message\":\"accepted request\",\"members\":[\"Ted\",\"Marx\"]}"
Right (JsonObject [("success",JsonBool True),("message",JsonString "accepted request"),("members",JsonArray [JsonString "Ted",JsonString "Marx"])])
ghci > parse jsonValue "test" "{foo}"
Left "test" (line 1, column 2):
unexpected "f"

10.空白への対応

ここまでのパーサーだと、次のようなカンマの前後にブランクがあるようなパターンのパースで不都合がある。

[true, true , true, true]

そこで、ホワイトスペース対応をおこなう。

まず、ホワイトスペースを検出するパーサーを作る

ws:: Parser String
ws = many $ oneOf " \t\n"

区切り記号の箇所に上記のws関数を挟み込む

-- jsonのarrayをHaskellのリストに変換
array:: Parser [JsonValue]
array =
    (char '[')
    *> (jsonValue `sepBy` (ws *> char ',' <* ws))
    <* (char ']')

-- jsonのオブジェクトを検出してPairにする
objectEntry:: Parser (String, JsonValue)
objectEntry = do
    key <- stringLiteral
    ws *> char ':' <* ws
    value <- jsonValue
    return (key, value)

-- jsonのオブジェクトをHaskellのPairに変換
jsonObject:: Parser JsonValue
jsonObject =
    JsonObject <$>
        ((char '{')
        *> (objectEntry `sepBy` (ws *> char ',' <* ws))
        <* (char '}'))

これで次のようなよくあるJSONもValidであると判定されるようになった

ghci > parse jsonValue "test" "{\"success\" : true, \"message\" : \"accepted request\", \"members\" : [\"Ted\" , \"Marx\"]}"
Right (JsonObject [("success",JsonBool True),("message",JsonString "accepted request"),("members",JsonArray [JsonString "Ted",JsonString "Marx"])])

Numberは???

さて、最初に書いたYouTubeの動画ですが、JSONのNumber型のパーサーの書き方の説明がなかったので自力でやっているのですが、まだHaskellちから足りなくて、できてないという悲しい状況です。

おわり


【2015/06/15 23:18 追記】

ぺんぎんさんからコメント貰った。

どうやら、モジュールText.ParserCombinators.Parsecは今は使わないらしく、代わりにモジュールText.Parsecimportすればよいらしい