2
votes

I've defined a parser using Parsec, which has type Parsec Text () a for some a. I've also got a "deal with this chunk" function, which writes the thing I've parsed to a file and has type a -> IO (). The file format that it's parsing means that we get back to "top level" reasonably frequently.

Is there a way to take my original parser and "lift it" into the IO monad? I'm imagining something with the following type signature:

liftParser :: Parsec Text () a -> (a -> IO ()) -> ParsecT Text () IO ()

where the first argument is the pure parser and the second is the "do something with the thing I parsed" function.

Obviously, I can bodge together what I need by redefining my original parser in IO too, but that means my unit tests look horrible, and it just feels like the wrong approach.

Also, I can't do something crazy like calling runParserT because that would drop the source position information - if there's an error on line 1000 of the input, I'd like the error message to say so.

So is there a way to do this and, if so, how? Also, is this a sensible thing to do? I imagine that I'm at least managing to avoid accumulating the output data. And, assuming that I manage something like this, should I expect Parsec to manage to discard the input data that it's already dealt with?

1
Why not define your original parser polymorphically. ParsecT Text () m, leaving m undecided. Then your pure tests can test it on Identity, but you can instantiate at IO when you need to. - luqui
I've used luqui's approach with great results for unit testing before, and I'd definitely do it again. If you still find yourself wanting to call runParserT for whichever reason, you can simply use setPosition to set Parsec's idea of the current source location (without skipping in the stream). - that other guy
luqui: Good point, that's exactly what I should be doing! Could you reply with an answer? I'll happily accept it. - Rupert Swarbrick
@thatotherguy: Ah! Maybe that's actually a better approach for me - it will guarantee that Parsec doesn't need to hold on to any other internal state between top-levels. Thank you both. - Rupert Swarbrick

1 Answers

1
votes

Just so this question can be marked as answered: luqui's comment above explains how to do it.

The trick is to define all your parsers polymorphically, with type ParsecT Text () m (the definitions all end up looking like thing :: Monad m => Parsec Text () m MyType). Then you can instantiate with m the identity monad in the test-bench and equal to IO where they are used.