1

一般来说,我对 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

现在,我假设这些数字是整数。单独地,parseJString, parseJNumber, parseJBool, 和parseJNull在 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但是,当我尝试解析truefalse或时失败了,null我得到了一个有趣的错误。

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

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

4

2 回答 2

2

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

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

编辑:

不知何故,我认为你的组合(.)($)看起来有点奇怪。(>>=)当我懒得写括号时,我使用 (.) 可以摆脱函数参数(例如使用) 和 ($)。在您的函数中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

最后但并非最不重要的一点是,您可以轻松地将其他函数转换为使用(>>=)而不是 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
于 2013-07-24T06:35:37.613 回答
1

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

相比:

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

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

于 2013-07-24T06:36:04.543 回答