Question

I'm trying to simplify the below code that's part of an attoparsec parser for a network packet, and I'm at a loss for a nice way to do it.

It starts with a call to atEnd :: IO Bool to determine if there's more to parse. I can't find a nicer way to use atEnd than to unwrap it from IO and then use it in an if statement, but it seems like there must be be a simpler way to check bool inside a monad. Here's the code:

maybePayload :: Parser (Maybe A.Value)
maybePayload = do
    e <- atEnd
    if e then return Nothing
    else do
        payload' <- char ':' *> takeByteString
        maybe mzero (return . Just) (maybeResult $ parse A.json payload') 

The intention is to return Nothing if there is no payload, to return Just A.Value if there is a valid JSON payload, and for the parser to fail if there is a non-valid payload.

Here's the Packet record that eventually gets created:

data Packet = Packet
    { pID       :: Integer
    , pEndpoint :: String
    , pPayload  :: Maybe A.Value
    }
Was it helpful?

Solution

You're doing a lot of work you don't need to do. First you check if you're at the end of the data and return Nothing if that doesn't work out. That's just not necessary, because if you're at the end, any parser that requires content will fail, and using maybeResult will turn that failure into Nothing.

The only time your parser fails is with the case where the input has data which doesn't start with the character :, the rest of the time it succeeds, even if that's by returning Nothing.

The only actual parsing going on is checking for : then using A.json. I think you're trying to write the whole program inside one parser, whereas you should just do the parsing on its own then call that as necessary. There's no need to check for end of data, or to make sure you get the whole content - that's all built in for free in a parser. Once you get rid of all that unnecessary checking, you get

payload :: Parser A.Value
payload = char ':' *> A.json

If you want to you can use that as maybeResult $ parse payload input to get a Maybe A.Value that's not additionally wrapped in a Parser. If you don't apply maybeResult, you can pattern match on the Result returned to deal separately with Failure, Partial success and Success.


Edit: OK, clearer now, thanks:

  • (If there's a colon followed by invalid json, fail)
  • If there's a colon followed by valid json, succeed, wrapping it in Just
  • If there's just end of input, succeed, returning Nothing

So we get:

maybePayload :: Parser (Maybe A.Value)
maybePayload = char ':' *> (Just <$> A.json)
            <|> (Nothing <$ endOfInput)

I've used <$> and <$ from Control.Applicative, or if you prefer, from Data.Functor.

<$> is an infix version of fmap, so Just <$> A.json does A.json and wraps any output in Just.

<$ is fmap.const so replaces the () from endOfInput with Nothing.

OTHER TIPS

Why you need to encode failure in Maybe when the parser monad already has a built-in notion of failure? The problem with using Maybe in this way is that the parser cannot backtrack.

You could try something like this (I haven't tried to typecheck it) and then use option in the caller:

payload :: Parser Value
payload = do
  payload' <- char ':' *> takeByteString
  let res = parse A.json payload'
  case res of
    Error msg -> fail msg
    Success a -> return a
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top