Actually, in Haskell (map print) is pure; it just doesn't do what you expect it to:
>>> map print ["hello","world","!"]
<interactive>:2:1:
No instance for (Show (IO ())) arising from a use of `print'
Possible fix: add an instance declaration for (Show (IO ()))
In a stmt of an interactive GHCi command: print it
What's going on here? Let's look at the type of (map print) to find out:
>>> :t (map print)
(map print) :: Show a => [a] -> [IO ()]
(map print) is a function which takes a list of values of type
a -- such that a can be
shown as a String; hence the
Show a => constraint -- and returns a list of values of type
IO () (pronounced IO unit). These are monadic values representing computations that perform the actual IO. Hence, (map print) is a pure function which carries no side effects.
So, what the heck do we do with this strange list of IO ()s? Well, one answer is to pass them to sequence_:
>>> sequence_ (map print ["hello","world","!"])
"hello"
"world"
"!"
Ahhh, so now we get to the impure function: sequence_! Actually, sequence_ is a pure function as well. Its type is:
>>> :t sequence_
sequence_ :: Monad m => [m a] -> m ()
sequence_ merely takes a list of monadic values and combines them into a single monadic value, discarding any of the elements' return values and returning () instead.
So if everything is a pure function, how do we actually perform the side effects? The simplest way to think of it is that our whole program is a bunch of pure functions which construct a single value representing all of the side effects that will take place over the lifetime of the program. This single value is called main:
main :: IO ()
main = sequence_ (map print ["hello","world","!"])
With this idea in mind, we can think of Haskell's runtime as taking this single value
main and performing the side effects specified throughout our program.