Question

I'm still pretty new to Haskell and functional programming in general, so I'm writing a small program with Parsec to parse JSON and pretty print it as a means of learning basic concepts. This is what I have so far:

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

For now, I'm assuming that the numbers are integers. Individually, parseJString, parseJNumber, parseJBool, and parseJNull work as expected in ghci. Additionally, parseJValue correctly parses strings and numbers.

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 fails, however, when I try to parse true, false, or null, and I get an interesting error.

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

I get a successful parse, but the parse returns a JNumber followed by an error stating that Prelude.read failed. I feel like I'm missing some core concept in building my parsers, but I can't see where I've gone wrong. Also, am I making any beginner mistakes with my code, i.e. would any of this be considered "bad" haskell?

Was it helpful?

Solution

The problem is the usage of many in parseJNumber. It is also a valid parse, when no character of the following string is consumed ("many p applies the parser p zero or more times. [...]"). What you need is many1:

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

Edit:

Somehow, I think your combination of (.) and ($) looks kind of weird. I use (.) when I can get rid of a function parameter (like in the usage of (>>=)) and ($) when I'm to lazy to write parentheses. In your function parseJString you do not need (.) in order to get the right binding precedences. (I did the same transformation in the code above.)

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

Additionally, you could eliminate code-repetition by refactoring parseJBool:

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

I would even rewrite the case-construct into a (total) local function:

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

Last but not least, you can easily transform your other functions to use (>>=) instead of 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

OTHER TIPS

You should try with many1 digit rather than many digit. A many succeeds on zero occurrences of the argument.

Compare:

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

So in your case, parseJNumber within parseJValue will succeed and return an empty string which is then passed to read. But read "" :: Double fails.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top