Search code examples
haskellmonadsparsec

How to simplify a Parsec rule which modifies user state


In learning Parsec, I've found that somewhat verbose rules like

type PhonemeClassMap = Map Char String
type ContextElement = Parser String

phonemeContext :: Parsec String PhonemeClassMap ContextElement
phonemeContext = do
    c <- lower
    return $ char c

can be simplified by lifting functions like char into the Parsec / ParsecT monad.

phonemeContext :: Parsec String PhonemeClassMap ContextElement
phonemeContext = liftM char lower

Now I'm trying to simplify a rule which modifies the user state:

import Data.Map (insert)

phonemeClassDefinition :: Parsec String PhonemeClassMap ()
phonemeClassDefinition = do
    upperChar <- upper
    lowerChars <- char ':' >> spaces >> many1 lower
    modifyState (insert upperChar lowerChars)

I can easily lift insert :: Char -> String -> PhonemeClassMap -> PhonemeClassMap to make the following improvement:

phonemeClassDefinition = do
    f <- liftM2 insert upper (char ':' >> spaces >> many1 lower)
    modifyState f

Is there any way to state these two expressions as one? The same lifting technique does not work for modifyState :: Monad m -> (u -> u) -> ParsecT s u m ().


Solution

  • In this case, you are looking for monadic bind >>= :: Monad m => (a -> m b) -> m a -> m b which allows you to apply a function that takes a pure a and returns a monadic action to a monadic a (i.e. apply the function "through" the monad). This function is actually an integral part of the monadic type class, and is what the <- in do notation desugars to under the hood.

    (Side note, unlike liftM2, liftM3..., there doesn't appear to be predefined bindM2 :: Monad m => (a -> b -> m c) -> m a -> m b -> m c (or bindM3 etc) for convenience. (Hoogle draws a blank.))

    Also, Parsec parsers often benefit (stylistically and otherwise) from using its Applicative and Functor instances, not just its Monad one, specifically <$> (alias of fmap/liftM) and the various (semi-)equivalents of monadic ap & >>: <*>, <* & *>.

    phonemeContext = char <$> lower
    
    phonemeClassDefinition = (insert <$> upper <*> (char ':' *> spaces *> many1 lower)) >>= modifyState
    

    (Note that @melpomene's =<< is just flip (>>=), i.e. with the arguments swapped.)