Mail merge (Parsec string formatting) example My intent is to perform "mail merge" where I can write strings like "hi &lt;&lt;name&gt;&gt;" and format them according to a HashMap.

Specifically, the string contains keys formatted as &lt;&lt;key&gt;&gt;, and the map contains the corresponding values.

I have a main concern that I'd like help with. I think it would be best to perform the parsing in multiple stages:


first find the Keys
second find the remaining Chunks
and for more complicated parsing tasks, perhaps more stages


I couldn't figure that out and instead use the more expensive lookahead function notFollowedBy and one pass. That obviously wouldn't work well if I had a slightly more complicated need.

import Data.Functor.Identity (Identity)
import Data.HashMap.Lazy as HM
import Text.Parsec
import Text.Parsec.String

-- Parsing ----------

data Merge a = Chunk a | Key a deriving (Show)

key :: Parser (Merge String)
key = Key &lt;$&gt; between (string "&lt;&lt;") (string "&gt;&gt;") (many1 letter)

chunk :: Parser (Merge String)
chunk = Chunk &lt;$&gt; many1 (notFollowedBy key &gt;&gt; anyChar)

prose :: ParsecT String () Identity [Merge String]
prose = many1 $ key &lt;|&gt; chunk

-- Formatting ----------

format :: HM.HashMap String String -&gt; [Merge String] -&gt; String
format _ []             = ""
format hmap (Chunk x : xs) = x ++ format hmap xs
format hmap (Key k : xs) = 
  case HM.lookup k hmap of
    -- I could obviate the `error` by working within a failure monad
    Nothing -&gt; error $ "missing key: " ++ k
    Just v -&gt; v ++ format hmap xs

-- Testing ----------

testString = "Hi &lt;&lt;name&gt;&gt;! Do you like &lt;&lt;thing&gt;&gt;?"
testMap = HM.fromList [("name", "Adam"), ("thing", "Apples")]
main = print $ format testMap &lt;$&gt; parse prose "" testString
Instead of notFollowedBy, you can use noneOf in chunk:

chunk :: Parser (Merge String)
chunk = Chunk &lt;$&gt; many1 (noneOf "&lt;") &lt;|&gt; try (sequence [char '&lt;', noneOf "&lt;"])


That prevents a lookahead but it is no longer that nice to look at. 



format can be rewritten without explicit recursion if we use concatMap:

format :: HM.HashMap String String -&gt; [Merge String] -&gt; String
format hmap xs = concatMap go xs
  where
    go (Chunk c) = c
    go (Key   k) = maybe (error $ "missing key: " ++ k) id (HM.lookup k hmap)


However, this would make our function partial, which we try to prevent. So let's use Either instead:

format :: HM.HashMap String String -&gt; [Merge String] -&gt; Either String String
format hmap xs = concat &lt;$&gt; mapM go xs
  where
    go (Chunk c) = Right c
    go (Key   k) = maybe (Left $ "missing key: " ++ k) Right (HM.lookup k hmap)