> And you end up writing stuff like main = do { putStrLn "Hello, World!" ; return () } which to me looks like C. There must be a more functional approach.
It looks imperative because that's what Haskell's do-notation was designed to. But it's merely syntactic sugar for the functional equivalent
main = putStrLn "Hello, World!" >> return ()
which btw is equivalent to simply main = putStrLn "Hello, World!"