Take the following mini-language:
data Action = Get (Char -> Action) | Put Char Action | End
Get f
means: read a character c
, and perform action f c
.
Put c a
means: write character c
, and perform action a
.
Here's a program that prints "xy", then asks for two letters and prints them in reverse order:
Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))
You can manipulate such programs. For example:
conditionally p = Get (\a -> if a == 'Y' then p else End)
This is has type Action -> Action
- it takes a program and gives another program that asks for confirmation first. Here's another:
printString = foldr Put End
This has type String -> Action
- it takes a string and returns a program that writes the string, like
Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End))))
.
IO in Haskell works similarily. Although executing it requires performing side effects, you can build complex programs without executing them, in a pure way. You're computing on descriptions of programs (IO actions), and not actually performing them.
In language like C you can write a function void execute(Action a)
that actually executed the program. In Haskell you specify that action by writing main = a
. The compiler creates a program that executes the action, but you have no other way to execute an action (aside dirty tricks).
Obviously Get
and Put
are not only options, you can add many other API calls to the Action data type, like operating on files or concurrency.
I didn't say anything about monads. Action
is not a monad. Monads are only some combinators, like conditionally
above, and are not important to purity.
The essential thing about purity is referential transparency. In Haskell, if you have f x+f x
you can replace that with 2*f x
. In C, f(x)+f(x)
in general is not the same as 2*f(x)
, since f
could print something on the screen, or modify x
. The advantages are mentioned here: basically it allows better optimalizations, since the compiler has much more freedom.