When you grok the IO type ("monads") it stops feeling like jumping through hoops, and then it isn't any harder to write imperative code in it than in any normal, imperative language.
Haskellers think I/O is an important effect and want to manage when it's allowed. It's allowed in the IO context, which happens to be the default:
main = do
putStrLn "hello world"
more
more = putStrLn "hello again, still in IO"
If you want to, you can break the rule and do I/O from non-IO functions, by using trace:
import Debug.Trace
add :: Int -> Int -> Int
add a b =
trace ("adding " ++ show a ++ " and " ++ show b)
(a + b)
or unsafePerformIO:
import System.IO.Unsafe
add :: Int -> Int -> Int
add a b = unsafePerformIO $ do
appendFile "debug.log" ("adding " ++ show a ++ " and " ++ show b ++ "\n")
return (a + b)
Because everything can do IO and launch missiles there :) And there's no way to limit it (except maybe algebraic effects introduced recently? I'm pretty sure nobody uses them anyway)