views:

219

answers:

3

I'm trying to use MonadError together with Parsec. I've come up with the following code snippet:

f5 = do
    char 'a'
    throwError "SomeError"

f6 = f5 `catchError` (\e -> unexpected $ "Got the error: " ++ e)

ret = runErrorT (runParserT f6 () "stdin" "a")

However, ret is Left "SomeError", it seems the catchError doesn't have any effect. What's the right way to use MonadError here?

I'd prefer to use MonadError over Parsec's own error handling, as for example when I have:

try (many1 parser1) <|> parser2

If parser1 fails here, parser2 will continue, but I'd like to have an exception which aborts the parsing entirely.

+3  A: 

I'm under the impression that you're trying to involve MonadError for the wrong reason.

In the try (many1 parser1) <|> parser2, the behaviour you're trying to avoid stems from the use of try and <|> -- if you don't like it, use different combinators. Perhaps an expression like (many1 parser1) >> parser2 would work better for you? (This discards the results from (many1 parser1); you could of course use >>= and combine the results from (many1 parser1) with those from parser2.)


(Note: Below this point, there is no really good solution to the problem at hand, just some musings as to why some things probably won't work... Hopefully this may be (somewhat) enlightening, but don't expect too much.)

A closer examination of ParsecT / MonadError interaction. I'm afraid it's a bit messy and I'm still not really sure how best to go about doing what the OP wants to do, but I'm hoping the following will at least provide insight into the reasons for the lack of success of the original approach.

Firstly, note that it is not correct to say that Parsec is an instance of MonadError. Parsec is the monad produced by ParsecT when the inner monad is Identity; ParsecT produces instances of MonadError if and only if it is given an inner monad which is itself an instance of MonadError to work with. Relevant fragments of GHCi interactions:

> :i Parsec
type Parsec s u = ParsecT s u Identity
    -- Defined in Text.Parsec.Prim
-- no MonadError instance

instance (MonadError e m) => MonadError e (ParsecT s u m)
  -- Defined in Text.Parsec.Prim
-- this explains why the above is the case
-- (a ParsecT-created monad will only become an instance of MonadError through
-- this instance, unless of course the user provides a custom declaration)

Next, let's have ourselves a working example with catchError and ParsecT. Consider this GHCi interaction:

> (runParserT (char 'a' >> throwError "some error") () "asdf" "a" :: Either String (Either ParseError Char)) `catchError` (\e -> Right . Right $ 'z')
Right (Right 'z')

The type annotation appears necessary (this seems to make intuitive sense to me, but it isn't pertinent to the original question, so I won't try to elaborate). The type of the whole expression is determined by GHC to be as follows:

Either String (Either ParseError Char)

So, we've got a regular parse result -- Either ParseError Char -- wrapped in the Either String monad in place of the usual Identity monad. Since Either String is an instance of MonadError, we can use throwError / catchError, but the handler passed to catchError must of course produce a value of the correct type. That's not very useful for breaking out of the parsing routine, I'm afraid.

Back to the example code from the question. That does a slightly different thing. Let's examine the type of ret as defined in the question:

forall (m :: * -> *) a.
(Monad m) =>
m (Either [Char] (Either ParseError a))

(According to GHCi... note that I had to lift the monomorphism restriction with {-# LANGUAGE NoMonomorphismRestriction #-} to have the code compile without type annotations.)

That type is a hint as to the possibility of doing something amusing with ret. Here we go:

> runParserT ret () "asdf" "a"
Right (Left "some error")

In hindsight, the handler given to catchError produces a value using unexpected, so of course it's going to be (usable as) a parser... And I'm afraid I don't see how to hammer this into something useful for breaking out of the parsing process.

Michał Marczyk
I'd like to abort the parsing process at some arbitrary point, "try" was just an example.
Johannes Bittner
Michał Marczyk
Why not using MonadError?
Johannes Bittner
Well, I'm not sure if this will help you any, but I've tried to analyse what's going on and edited the findings into the answer... If this is helpful, great, if not, let me know and I might just delete the whole thing. The thing about Parsec not actually being an instance of MonadError (as opposed to some other monads one can construct with ParsecT) is rather important to realise, though.
Michał Marczyk
+1  A: 

If you're trying to debug a parser to troubleshoot, it's probably simpler to use error, Debug.Trace, or whatnot.

On the other hand, if you need to terminate the parsing on some inputs as part of your actual program, but it's not doing so because of a try (...) <|> construct, then you have a bug in your logic and you should stop and rethink your grammar, rather than hack around it with error handling.

If you want the parser to terminate on a given input some of the time, but not others, then either something is missing from your input stream (and should be added) or a parser is not the solution to your problem.

If you want the parser to recover gracefully from non-fatal errors and keep trying when possible, but terminate with an error when it can't continue, then you... may want to consider something other than Parsec, because it's really not designed for that. I believe Utrecht University's Haskell parser combinator library supports that sort of logic much more easily.

Edit: As far as Parsec being itself an instance of MonadError goes--yes, and its own error handling subsumes that functionality. What you're trying to do is stack a second error monad on top of Parsec, and you're probably having trouble because it's generally awkward to distinguish between monad transformers that are "redundant" in that manner. Dealing with multiple State monads is more famously awkward, which is why Parsec (a State monad as well) provides functionality to hold custom state.

In other words, Parsec being an error monad doesn't help you at all, and in fact is relevant mostly in the sense of making your problem more difficult.

camccann
Why isn't it designed for that? I mean, Parsec is an instance of `MonadError`. I don't want to do hacks like you mentioned, there are just some cases where I want to abort the parsing process using exceptions. To be honest, I'd be more interested in why my code doesn't work than in why I shouldn't use MonadError at all.
Johannes Bittner
It isn't designed for general error recovery because that's a very difficult problem in parsing; Parsec generally works under the assumption that a parser either succeeds, fails harmlessly and immediately, or fails unrecoverably (due to consuming input). Parsec's own error handling should be sufficient for all of these cases; if you need to go beyond that, it's likely that Parsec isn't well-suited to your task.
camccann
+2  A: 

if you need to terminate the parsing on some inputs as part of your actual program, but it's not doing so because of a try (...) <|> construct, then you have a bug in your logic and you should stop and rethink your grammar, rather than hack around it with error handling.

If you want the parser to terminate on a given input some of the time, but not others, then either something is missing from your input stream (and should be added) or a parser is not the solution to your problem.

This answer is based on the assumption that the problem lies in the grammar. But if I'm using the grammar to feed a compiler, there are other errors that a grammar can't handle. Let's say a variable reference, to a variable that wasn't defined. And the language is specified as a single pass, and variables are evaluated as encountered. Then, the grammar is just fine. The parsing is just fine. But as a result of evaluating what was specified in the grammar an error has occurred, the existing "fail" or "unexpected" or insufficient to deal with this problem. It would be nice to have a means to abort the parsing without resorting to higher level error handling.

Shawn Garbett