Search code examples
haskellparsec

An easy way to change the type of Parsec user state?


I'm looking for an easy way to combine two parts of ParsecT code that have the same stream and monad, but different user state and outcome. Essentially a function like this would be nice:

withUserState :: u -> ParsecT s u m a -> ParsecT s v m a

The thing is, that the user state is really helpful in some cases, but I need different states at different times and don't want to make the states type any bigger. Do I have to modify State somehow to achieve this, or is there already a function for it that I can't find at the moment?

Edit:

I think an alternative would be something like

changeUserState :: (u -> v) -> ParsecT s u m a -> ParsecT s v m a

Solution

  • Parsec doesn't let you do this directly out of the box, but you can achieve this using Parsec's public API as follows:

    {-# LANGUAGE ScopedTypeVariables #-}
    
    import Text.Parsec
    
    changeState
      :: forall m s u v a . (Functor m, Monad m)
      => (u -> v)
      -> (v -> u)
      -> ParsecT s u m a
      -> ParsecT s v m a
    changeState forward backward = mkPT . transform . runParsecT
      where
        mapState :: forall u v . (u -> v) -> State s u -> State s v
        mapState f st = st { stateUser = f (stateUser st) }
    
        mapReply :: forall u v . (u -> v) -> Reply s u a -> Reply s v a
        mapReply f (Ok a st err) = Ok a (mapState f st) err
        mapReply _ (Error e) = Error e
    
        fmap3 = fmap . fmap . fmap
    
        transform
          :: (State s u -> m (Consumed (m (Reply s u a))))
          -> (State s v -> m (Consumed (m (Reply s v a))))
        transform p st = fmap3 (mapReply forward) (p (mapState backward st))
    

    Note that it requires both forward and backward conversion between u and v. The reason is that first you need to translate your ambient state to the local state, run your inner parser, an then convert back.

    ScopedTypeVariables and local type signatures are there just for clarity — feel free to remove them if you like.