mike-neckのブログ

Java or Groovy or Swift or Golang

やっとこさ、JSONをパースするHaskellのコードほぼ完成した

以前、書いた記事でJSONの数値型のパーサーが書けなくて、悩んでたやつ、なんとか汚いながらも動く奴が書けた。

これが以前のやつ

mike-neck.hatenadiary.com

JSONの数値型のパーサー部分が書けなかったのは、数値には正と負およびintとfloatという4つの状態をもつ組み合わせがあって、それらの状態を扱うためのMonadちからとかMaybeちからが足りなかったから。なお、今回書き上がったけど、まだMonadちからとかMaybeちからとかApplicativeちからとかは不十分だと思ってる。


インポートする奴

なんか、ぺんぎんの人から、Text.ParserCombinatorを使うのは古いと教えてもらったので、あらためてインポートするパッケージをText.Parsecに変更した。あと、追加でData.MonoidData.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をうまく扱えてなかっただけという感じ。MonadApplicative云々は小さくコードを書き続けていればそのうち何とかなりそうな気はします。あと、コンパイラーが優秀なので、コンパイラーが大量のエラーを吐いているからといって、怖気づかずに、コンパイラーに教えてもらう感じでやってると書けるようになるのではと思い上がってみたりした。結局のところは、コードを書けって話ですね。


全体のコードはこちらからどうぞ。

jsonをパースするhaskellのコード