f x y z = do
let
foo = x + y
bar = foo * z
putStrLn $ "Here's bar" <> (show bar)
pure bar
If all you want is to log to the console for debugging from pure code, it would look like
f x y z =
let
foo = x + y
bar = foo * z
in
trace ("Here's bar" <> (show bar)) bar
Not recommended, but if you're dead set on commingling your effectful and pure code, you can freely mix them with the function unsafePerformIO.
In my experience having to write large applications with significant test coverage, the time savings from not screwing around with stubs and mocks and dealing with the external effects of test runs -- the relative annoyance of using a couple "do"s and "pure"s and separating out data transformation from IO is a small price to pay.
Ah, so now your original statement becomes clearer! When you said
> Look at all the monad insanity Haskell has to do to get the equivalent of a print statement
perhaps you really meant
> Look at all the monad insanity Haskell has to do to get the equivalent of a print statement within pure code
I still don't agree as such, but yes, there is indeed ceremony involved in turning something that was pure into something effectful. That is in fact the whole point!