I came to understand these things using the following analogy, which I'll express with JavaScript.
How can one express a side-effecting computation?
1. A function
That is obviously a first thing comming to mind:
var launchRockets = function () {
prepareRockets( queryDBForPreparationParameters() )
launchAllPreparedRockets()
outputResults()
}
You can see an effectful function calling a bunch of other effectful functions, which themselves can produce unknown effects with all the ensuing consequences.
2. Instructions
Another way to express this would be to compose a set of instructions describing those effectful computations for some function to later execute. (Ever composed an SQL query?)
var launchRocketsInstructions = [
{
description: "Prepare rockets",
parameters: {
description: "Query a DB for preparation parameters"
}
},
{
description: "Launch all prepared rockets"
},
{
description: "Output results"
}
]
So what do we see in our second example? We see an immutable data tree describing the computation instead of performing it right away. There are no side effects here, and for composing this data tree we can surely use pure functions. And that's what essentially side effects are all about in Haskell. All the infrastructure the language provides: the monads, the IO
, the do
-notation - these are just tools and abstractions simplifying your task of composing a single tree of instructions.
Of course to actually perform these instructions one will have to eventually escape into the wild world of side-effects. In case of JavaScript it would be something like execute(launchRocketsInstructions)
, in case of Haskell it is the runtime executing the root of the instruction tree which you produce with the function main
of the main module, which becomes the single entry point of your program. Thereby the side effects in Haskell actually occur outside of the language scope, that's why it's pure.