【问题标题】:Json parser, incorrectly parsing string as a numberJson解析器,错误地将字符串解析为数字
【发布时间】:2013-07-24 06:22:49
【问题描述】:

总的来说,我对 Haskell 和函数式编程还是很陌生,所以我正在用 Parsec 编写一个小程序来解析 JSON 并漂亮地打印它作为学习基本概念的一种手段。这是我目前所拥有的:

import Text.Parsec
import Text.Parsec.String

data JValue = JString String
            | JNumber Double
            | JBool Bool
            | JNull
            | JObject [(String, JValue)]
            | JArray [JValue]
              deriving (Eq, Ord, Show)

parseJString, parseJNumber, parseJBool, parseJNull :: Parser JValue
parseJString = do
    str <- between (char '"') (char '"') (many (noneOf "\""))
    return . JString $ str

parseJNumber = do
    num <- many digit
    return . JNumber . read $ num

parseJBool = do
    val <- string "true" <|> string "false"
    case val of
        "true"  -> return (JBool True)
        "false" -> return (JBool False)

parseJNull = string "null" >> return JNull

parseJValue :: Parser JValue
parseJValue =   parseJString 
            <|> parseJNumber 
            <|> parseJBool 
            <|> parseJNull

现在,我假设这些数字是整数。单独地,parseJStringparseJNumberparseJBoolparseJNull 在 ghci 中按预期工作。此外,parseJValue 可以正确解析字符串和数字。

ghci> parse parseJString "test" "\"test input\""
Right (JString "test input")
ghci> parse parseJNumber "test" "345"
Right (JNumber 345.0)
ghci> parse parseJBool "test" "true"
Right (JBool True)
ghci> parse parseJNull "test" "null"
Right JNull
ghci> parse parseJValue "test" "\"jvalue test\""
Right (JString "jvalue test")
ghci> parse parseJValue "test" "789"
Right (JNumber 789.0)

parseJValue 失败,但是,当我尝试解析 truefalsenull 时,我收到了一个有趣的错误。

ghci> parse parseJValue "test" "true"
Right (JNumber *** Exception: Prelude.read: no parse

我得到一个成功的解析,但是解析返回一个JNumber,后面跟着一个错误,指出 Prelude.read 失败。我觉得我在构建解析器时缺少一些核心概念,但我看不出哪里出错了。另外,我的代码是否犯了任何初学者错误,即这些是否会被视为“坏”haskell?

【问题讨论】:

    标签: json haskell parsec


    【解决方案1】:

    问题在于manyparseJNumber 中的使用。当没有使用以下字符串的任何字符时,它也是一个有效的解析(“许多 p 应用解析器 p 零次或多次。[...]”)。你需要的是many1:

    parseJNumber = do
      num <- many1 (oneOf "0123456789")
      return $ JNumber (read num :: Double)
    

    编辑:

    不知何故,我觉得你的(.)($) 的组合看起来有点奇怪。当我可以摆脱函数参数时(例如 (&gt;&gt;=) 的用法)我使用 (.) 和当我懒得写括号时使用 ($)。在您的函数parseJString 中,您不需要(.) 来获得正确的绑定优先级。 (我在上面的代码中做了同样的转换。)

    parseJString = do
      str <- between (char '"') (char '"') (many (noneOf "\""))
      return $ JString str
    

    此外,您可以通过重构 parseJBool 来消除代码重复:

    parseJBool = do
      val <- string "true" <|> string "false"
      return (case val of
        "true"  -> JBool True
        "false" -> JBool False)
    

    我什至会将 case-construct 重写为(全部)局部函数:

    parseJBool = (string "true" <|> string "false") >>= return . toJBool
     where
      -- there are only two possible strings to pattern match
      toJBool "true" = JBool True
      toJBool _      = JBool False
    

    最后但同样重要的是,您可以轻松地将其他函数转换为使用 (&gt;&gt;=) 而不是 do-blocks。

    -- additionally, you do not need an extra type signature for `read`
    -- the constructor `JNumber` already infers the correct type
    parseJNumber =
      many1 (oneOf "0123456789") >>= return . JNumber . read
    
    parseJString =
      between (char '"') (char '"') (many (noneOf "\"")) >>= return . JString
    

    【讨论】:

    • 我绝对同意您建议的重构更改,但是 vim 中的 Syntastic 给了我一个相当模糊的警告,让我在使用 &gt;&gt;= return 时改为使用 liftM。是否有理由改变这一点,还是应该忽略它?
    • 您可能应该使用liftM f x,它完全等同于x &gt;&gt;= return . f。或者更好的是,您可以使用fmap f x(只要您有Functor 实例就可以使用)或f &lt;$&gt; x(只要您有Applicative 实例就可以使用)。
    【解决方案2】:

    您应该尝试使用many1 digit 而不是many digitmany 在参数出现零次时成功。

    比较:

    ghci> parse (many digit) "test" "true"
    Right ""
    ghci> parse (many1 digit) "test" "true"
    unexpected "t"
    expecting digit
    

    因此,在您的情况下,parseJValue 中的parseJNumber 将成功并返回一个空字符串,然后将其传递给read。但是read "" :: Double 失败了。

    【讨论】:

      猜你喜欢
      • 2013-05-10
      • 1970-01-01
      • 2011-03-14
      • 2016-04-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多