Search code examples
haskellerror-handlingtext-parsingparsec

How can I force Parsec to return an error?


I'm making a parser with Parsec and I try to return a specific error during the parsing.

This is a minimal parser example to expose my problem :

parseA = try seq1
      <|>  seq2

seq1 = do
          manyTill anyChar (try $ string "\n* ")
          many1 anyChar
          fail "My error message" 

seq2 = do
          manyTill anyChar (try $ string "\n- ")
          many1 anyChar

I would like to perform some tests in the first try $ do sequence and stop the parsing and return a specific error message. When I don't use fail I get :

ghci>  parse parseA  "" "aaaaaa\nbbbb\n* ccccc\n- ddd"
Right "ccccc\n- ddd"

When I use fail or unexpected, my parser doesn't stop (due to the try function) and execute the next do sequence:

ghci>  parse parseA  "" "aaaaaa\nbbbb\n* ccccc\n- ddd"
Right "ddd"

And it's not what I want!

I considered using the basic error function to stop the execution of my parser but I would like to have a "clean" error returned by the parsing function like this:

ghci>  parse parseA  "" "aaaaaa\nbbbb\n* ccccc\n- ddd"
Left "My error message"

Do you know how to properly stop a parser and return a custom error?


Solution

  • If you want the monad to behave differently then perhaps you should build a different monad. (N.B. I'm not entirely clear what you want, but moving forward anyway).

    Solution: Use a Monad Transformer Stack

    For example, to get a fail-like function that isn't caught and ignored by Parsec's try you could use an Except monad. Except allows you to throw errors much like exceptions but they are plumbed monadically instead of using the actual exception mechanism which demands IO to catch it.

    First, lets define our monad:

    import Text.Parsec
    import Text.Parsec.Combinator
    import Text.Parsec.Char
    import Control.Monad.Trans.Except
    import Control.Monad.Trans
    
    type EscParse a = ParsecT String () (Except String) a
    

    So the monad is EscParse and combines features of Parsec (via the transformer ParsecT) and Except.

    Second, let's define some helpers:

    run :: EscParse a -> SourceName -> String -> Either String (Either ParseError a)
    run op sn input = runExcept (runPT op () sn input)
    
    escFail :: String -> EscParse a
    escFail = lift. throwE
    

    Our run is like runParse but also runs the except monad. You might want to do something to avoid the nested Either, but that's an easy cosmetic change. escFail is what you'd use if you don't want the error to be ignored.

    Third, we need to implement your parser using this new monad:

    parseA :: EscParse String
    parseA = try seq1 <|>  seq2
    
    seq1 :: EscParse String
    seq1 = do manyTill anyChar (try $ string "\n* ")
              many1 anyChar
              escFail "My error message"
    
    seq2 :: EscParse String
    seq2 = do manyTill anyChar (try $ string "\n- ")
              many1 anyChar
    

    Other than spacing and type signature, the above matches what you had but using escFail instead of fail.