POSTDというサイトに次のようなエントリーがあった。
で、この記事の中でParsecライブラリーの紹介としてJSONパーサーを作るという動画が紹介されていた。
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 BoolのBoolがTrueのものを返す関数で、これ単独では特に使うことはない。
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
ここで、演算子(<$>)はFunctorのfmapみたいなもの(といういい加減な理解)。
(<$>) :: 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 追記】
ぺんぎんさんからコメント貰った。
@mike_neck 前にこういうついーとをみかけました https://t.co/saPkQ6iiuT
— なかやん@重機もみあげ (@pocketberserker) 2015, 6月 15
どうやら、モジュールText.ParserCombinators.Parsecは今は使わないらしく、代わりにモジュールText.Parsecをimportすればよいらしい