Fresh in both languages. But on studying the Effective Haskell book, there's a whole chapter (chp 14) on learning how to read and write efficient and faster Haskell code. Training that mental model of understanding how IO gets evaluated didn't seem too difficult. It feels akin to remembering how to write fast SQL code - you just practice a bit and measure.
In chp 7 on understanding IO, there's also another great section that explains how IO evaluation is often confused. I share an example from the book that reads, writes, and prints files.
Memory intensive func because it attempts to read _all_ files at once because the execution of reading and writing actually only happens when the print fn is called (putStrLn files). makeAndReadFile is actually doing both reading and writing - a normal task in all other languages.
slow =
let files = mapM makeAndReadFile [1..500] :: IO [ String ]
in files >>= (putStrLn . show)
Efficent version. Here we force reads and writes to actually occur per file instead of waiting til the print. Seems like a pretty easy step to misunderstand.
safe :: IO () safe =
foldl ( \ io id -> io >> makeAndShow id ) (return ()) [1..500]