How can passing the `IO ()` to `main` be considered pure?

102 Views Asked by At

I don't quite understand how printing (outputting to the screen) can be considered pure in a programming languages sense, but I was claimed to that such a concept exists in Haskell.

You create an IO () object via print, and you pass it to main, which does the printing for you.

My question: how can invoking main to perform such an impure action in itself be considered pure? Even if you claim main exists outside your Haskell program, you are still invoking it to perform a side effect.

1

There are 1 best solutions below

0
leftaroundabout On

I think the crucial point of confusion is this, from your comments:

Doesn't that definition of main have that side effect during execution of your code?

No it doesn't! Defining main doesn't have any side effects. –Well, defining something generally doesn't mean much anyway in lazy Haskell, but also forcing the main action to normal form wouldn't have any side effects:

ghci> let sideEffect :: IO (); sideEffect = putStrLn "I'M HAVING AN EFFECT!"
ghci> sideEffect `seq` 0
0

See, nothing is printed.

(This demonstration isn't all that meaningful; seq only forces to weak head normal form and may in fact not force anything at all if the compiler can prove this doesn't change anything about strictness properties. But the actual argument stands.)

The happening-of-side-effects is completely separate from the pure things going on in the Haskell definitions. As already said, what you're doing purely is defining a recipe for how side effects are supposed to happen. This doesn't mean they ever actually will happen. It's like a criminal making up a plan for a bank robbery whilst in prison: he can scheme as evil doings as he wishes, but nothing is actually going to happen because he can't leave the prison.

Running the program that contains this main action is a completely different story. Now any side effects that our recipe mentions will get unleashed on the world.

You might now ask: isn't this just the same in other programming languages? After all, compiling a program in C or Java also doesn't launch the side effects, only running the compiled program does.
The difference is that in Haskell, you can still evaluate functions and do computations and all whilst being sure side effects won't happen. This is handy because you can try out stuff in GHCi, or unit-test individual functions, or use a web framework dealing with user-defined code, and rest asured that no side effects or dependence on global state will muddle the results.
By contrast, in C, when evaluating a function (of which you only know the signature) you can never know whether it will also print something to the terminal or connect to a remote server or whatnot. (Well, you can sandbox it, but that's crude and unwieldy.)

Of course if you bring unsafePerformIO into the picture, all of it breaks down: with it, ordinary evaluations can also have side effects.

ghci> :m +System.IO.Unsafe 
ghci> let unsafeSideEffect :: (); unsafeSideEffect = unsafePerformIO (putStrLn "I'M HAVING AN EFFECT!")
ghci> unsafeSideEffect `seq` 0
I'M HAVING AN EFFECT!
0

But I don't consider unsafePerformIO to be really part of the Haskell language; it's an escape hatch mostly used for foreign-function calling and for low-level optimisations.