First, you should almost certainly use Data.Text's Text rather than String. You also may be interested in fromListWith from Data.Map.
You could write something like
countWords :: [Text] -> Map Text Int
countWords = fromListWith (+) . flip zip (repeat 1)
-- countWords ts = fromListWith (+) (zip ts (repeat 1))
As a side benefit, using this construction the result will be ordered by count, so you can skip sort'. Your sort comparison, by the way, can also be written comparing snd, rather than with a lambda.
Also, I’d highly recommend megaparsec over parsec, but that’s more preference than anything I can rigorously defend.
Avoid foldl. It causes a space leak in your program: your Map is not actually constructed until you go through the entire list. Replacing it with foldl’ should fix the problem.
I don’t know if I’d reach for Parsec or any other combinator library here. If you insist, Attoparsec would be much faster for this job.
Speaking of speed, you already heard about Text, but if your input is UTF-8, and since your logic ignores only ASCII characters, ByteString would also work. Don’t do this if you intend to handle other encodings or to extend your algorithm to cover Unicode spacing and punctuation.
Finally, you can eliminate the intermediate list. Since Parsec is not a streaming parser, the list will be fully constructed in memory. You can avoid that by constructing the map directly while parsing.
I agree, a quick words and filter out punctuation might do the job. But I also want easy ways to improve my Parser combinator knowledge.
Thanks for all the advice - it’s funny though, how Haskell is a honey trap for people like me looking for declarativity - but instead there’s a bunch of performance road bumps which need to be on one’s mind.
I suppose laziness giveth and taketh away - and perhaps over time, you don’t need to think too much about the performance details… Just some sort of cognitive dissonance going on…