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
すればよいらしい