我对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
,true
或false
时, null
失败了,我收到了一个有趣的错误。
ghci> parse parseJValue "test" "true"
Right (JNumber *** Exception: Prelude.read: no parse
我得到了一个成功的解析,但是解析返回JNumber
,后面跟一个错误说明Prelude.read失败了。我觉得我在构建解析器时缺少一些核心概念,但我无法看到我出错的地方。另外,我是否在使用我的代码时犯了任何初学者的错误,即是否会将这些错误视为“坏”的haskell?
答案 0 :(得分:2)
问题是在many
中使用parseJNumber
。当没有使用以下字符串的字符时,它也是一个有效的解析(“许多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重写为(total)local函数:
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
答案 1 :(得分:1)
您应该尝试使用many1 digit
而不是many digit
。 many
成功出现参数零。
比较
ghci> parse (many digit) "test" "true"
Right ""
ghci> parse (many1 digit) "test" "true"
unexpected "t"
expecting digit
因此,在您的情况下,parseJNumber
中的parseJValue
将成功并返回一个空字符串,然后传递给read
。但是read "" :: Double
失败了。