以前、書いた記事でJSONの数値型のパーサーが書けなくて、悩んでたやつ、なんとか汚いながらも動く奴が書けた。
これが以前のやつ
JSONの数値型のパーサー部分が書けなかったのは、数値には正と負およびintとfloatという4つの状態をもつ組み合わせがあって、それらの状態を扱うためのMonad
ちからとかMaybe
ちからが足りなかったから。なお、今回書き上がったけど、まだMonad
ちからとかMaybe
ちからとかApplicative
ちからとかは不十分だと思ってる。
インポートする奴
なんか、ぺんぎんの人から、Text.ParserCombinator
を使うのは古いと教えてもらったので、あらためてインポートするパッケージをText.Parsec
に変更した。あと、追加でData.Monoid
とData.Maybe.fromJust
をインポートした。
module Json.Parser( jsonValue ) where import Text.Parsec hiding ((<|>), many) import Text.Parsec.String (Parser) import Control.Applicative import Control.Monad import Data.Monoid import Data.Maybe (fromJust)
数値型のパースをサポートする奴
intとかfloatの文脈を扱いつつ、いい感じの型を使おうとしてMonad
とかいじくってたけど、別にこの型はMonad
である必要もなくて、Functor
ですらある必要がなかった…
-- Builder型 - json numberをパースするためのサポート型 data Builder = Builder { integral :: Parser (Maybe String), float :: Parser (Maybe String) } -- 'Which'型 - 'joinM'をサポートする型 data Which = Both | LeftOnly | RightOnly | Neither
Maybe a -> Maybe a -> Maybe a
のような型で、f Nothing (Just x) = Just x
になるような関数を探したけど、hoogle力足りなくて見つけられなかったので、自作した。また、Maybe a -> [a] -> Maybe [a]
のような関数も探したけど(ry
-- | 'joinM' takes 'Maybe a' and 'Maybe a' and joins it and returns 'Maybe a' with Constraint 'Monoid a' joinM :: (Monoid a) => Maybe a -> Maybe a -> (Which, Maybe a) joinM Nothing Nothing = (Neither, Nothing) joinM x Nothing = (LeftOnly, x) joinM Nothing x = (RightOnly, x) joinM (Just x) (Just y) = (Both, Just (x <> y)) -- | 'joinL' takes 'Maybe a' and '[a]' and returns 'Maybe [a]' joinL :: Maybe a -> [a] -> Maybe [a] joinL Nothing xs = Just xs joinL (Just x) xs = Just (x:xs)
もしあるようだったら、教えて下さい。
数値をパースする
数値をパースするのは次のような感じ。整数部分と小数部分を検出するパーサーは書けていたけど、小数部分をオプショナルで検出する方法が先週の段階でわかってなくて、optionMaybe
に辿り着いたのが先週中頃で、Maybe
をこねくり回す方法を考えてたのがこの前の土日。
-- | 'intLiteral' matchs integer starting 1 to 9 intLiteral :: Parser String intLiteral = (:) <$> oneToNine <*> many digit -- | 'oneToNine' returns matcher for 1 to 9 oneToNine :: Parser Char oneToNine = oneOf (concatMap show [1..9]) -- | 'intPart' matchs integer with sign intPart :: Parser (Maybe String) intPart = (joinL) <$> optionMaybe (char '-') <*> intLiteral -- | 'floatPart' matchs floating part of number floatPart :: Parser (Maybe String) floatPart = optionMaybe $ flp where flp = (:) <$> char '.' <*> many1 digit -- | 'builder' takes json number and returns 'Builder' builder :: Builder builder = Builder intPart floatPart -- | 'jsonNumber' takes json number and returns 'JsonValue' jsonNumber :: Parser JsonValue jsonNumber = maybeNumber builder -- | 'maybeNumber' takes 'Builder' and returns 'Maybe String' maybeNumber :: Builder -> Parser JsonValue maybeNumber (Builder x y) = do num <- joinM <$> x <*> y case num of (Neither, Nothing) -> JsonInt <$> pure 0 (LeftOnly, Just i) -> JsonInt <$> toInt i (RightOnly, Just f) -> JsonFloat <$> toFloat ('0':f) (Both, Just f) -> JsonFloat <$> toFloat f toInt :: String -> Parser Integer toInt i = pure $ read i toFloat :: String -> Parser Double toFloat f = pure $ read f
実行例
*Json.Parser> parse jsonValue "test" "{\"num\": 1.32, \"ints\" : [ 1, 2 , 3] , \"str\" : \"string\", \"obj\" : {\"key\" : \"value\"}}" Right (JsonObject [("num",JsonFloat 1.32),("ints",JsonArray [JsonInt 1,JsonInt 2,JsonInt 3]),("str",JsonString "string"),("obj",JsonObject [("key",JsonString "value")])])
まとめ
いろいろと難しく考えてたけど、結局のところMonad
をこねくり回すことはなくて、ずっとMaybe
をうまく扱えてなかっただけという感じ。Monad
やApplicative
云々は小さくコードを書き続けていればそのうち何とかなりそうな気はします。あと、コンパイラーが優秀なので、コンパイラーが大量のエラーを吐いているからといって、怖気づかずに、コンパイラーに教えてもらう感じでやってると書けるようになるのではと思い上がってみたりした。結局のところは、コードを書けって話ですね。
全体のコードはこちらからどうぞ。