Skip to content

Instantly share code, notes, and snippets.

@therewillbecode
Last active August 29, 2023 20:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save therewillbecode/cb342d28d79e0c24b3f950703d0da66f to your computer and use it in GitHub Desktop.
Save therewillbecode/cb342d28d79e0c24b3f950703d0da66f to your computer and use it in GitHub Desktop.
Blog Post on IO Monad.
12th April 2020

If Haskell is a pure functional language then how does anything get done?

Useful programs need to perform input/output or I/O if there are to interact with the outside world. How else would a program read files, print to the screen and interact with the external world. I/O is the process of responding to input signals from the external world and responding with output signals.

Haskell is a purely functional language. Let's define purity.

Purity is the property of functions which when given the same input always return the same output.

Pure functions can't modify anything externally. To do so would be performing a side effect. Think of side effects as outside effects from the point of view of inside a function.

Wait. Printing to the screen or making a network request require side effects.

This is where the confusion sets in.

Bewildered, some say OK everything in Haskell is pure apart from IO.

Yet the truth is rarely black and white and this case is no exception.

If you want to understand how IO is pure in Haskell you need to traverse different levels of abstraction. If you want to take the red pill read further. Otherwise you can take the blue pill and wake up as if nothing ever happened.

Still with me? Good!

Let's start by looking at an IO value. Look at the type of getLine.

    getLine :: IO String

Haskell uses pure values to represent effectful computations. This value is pure like 3. This is unusual amongst programming languages. Amongst programming languages, IO is not commonly first class. That is generally IO values doesn't have equal status to other values.

We know which pure values in are program can perform side effects when executed just by looking at the type signature.

It is a value of type IO String. IO tells us that side effects will happen when the program is executed. However the haskell program doesn't know anything about this to the Haskell compiler this is just a pure value like any other. Whereas String tells us that the type of the value which will be yielded by the I/O operation will be a when executed. This value represents an impure thing - reading a line of input - but in a pure way.

It doesn't make sense to talk about executing operations which can have side effects in Haskell. other operations such as 2 + 2. The language only knows about the evaluation. There is no notion of a side effect in Haskell.

Evaluation is what we do to expressions to produce values.

It is important to distinguish evaluation from execution of Haskell programs. Evaluating an IO value can't print to a screen or write to a file. You can evaluate an IO value without executing it.

IO is an interface between pure haskell programs and the impure programming language that is the runtime system.

In Haskell IO values are the interface to managing the impure I/O operations performed by the Haskell run time system. Think of the runtime as an interpreter. This is the sense in which Simon Peyton Jones jokingly refers to Haskell as "world's finest imperative programming language".

Although Haskell isn't imperative you can still use the language to do imperative things.

Haskell code once compiled taks with the operating system through the Haskell runtime. It is the runtime system wrapping our compiled Haskell program with uses system calls to interface with the OS and do impure things like IO such as writing to a file.

There is a common misconception that somehow monads enforce purity.

Monads don't enforce purity in any way.

In fact, purity isn't enforced in Haskell, rather impurity was never added in the first place.

From day one IO has been pure in Haskell, long before monadic I/O were introduced.

Before monadic I/O the C program which is our runtime system would repeatedly take requests for IO actions from the main function and feed them back in

    main :: [Response] -> [Request]

On first glance how think how could this ever have worked? How can responses to I/O operations come before requests. I/O is an acronym for input and output not the other way around.

The answer is laziness.

Now the topic of laziness would warrant a lengthy blog post, so I won't be able to fully explain it here. So here is a brief summary.

Lazy evaluation is a code execution mechanism which defers the evaluation of an expression until its value is needed.

Back then you would pair elements in [request] and [response] through their indexes. Same positions, same I/O operation. Because of laziness we can access one of the results of an IO operation in [response] even when the other values have not being computed yet. This all makes IO very unsafe as if you try and access a response which hasn't been computed then your program will crash at runtime.

The C program which comprises the "runtime system" wraps our program and we delegate all the impure operations to this C program which wraps the Haskell program.

Haskell programs were still pure then and they are now even though the type of our main function has changed to this

    main :: IO a 

The Haskell runtime knows what a side effect is and runs impure operations and does mutability all over the place. if we want to read a file then the C program which is the runtime makes a system call to the operating system. The haskell program is oblivious to this.

The base Haskell language is exclusively about evaluation. Execution is something that the runtime does.

"Progress is possible only if we train ourselves to think about programs without thinking of them as pieces of executable code." - E. Djikstra

Mainstream languages often look at programs as executable code. They don't draw a line between evaluation and execution of programs. Instead they accept that I/O can happen anywhere in a program. You can tell by the types in a Haskell program where IO can and cannot occur on execution.

So why separate evaluation and execution at all? Why is this model in any way more useful than the model where we blur the line and just say they are the same thing?

This separation is useful because then we can represent impure operations as pure values. This means that anything we can do with normal values we can do with these IO values representing impure operations.

As a result our language has more expressive power. For example we can write control structures that influence the control flow of a program as a library. Lets look at an example.

We can put IO values in a list in the same way as a collection of Int. Lets do for loops.

     for :: [a] -> (a -> IO ()) -> IO () 
     for [] fa = return () for (n:ns) fa = fa n >> for ns fa`

Here is a function which executes some IO actions in a list. Haskell itself knows nothing of for loops. Yet first class IO allows us to build any kind of control structure we like.

Remember >> is used to sequence monadic actions like >>= but the resulting value of each effectful computation is discarded. The body of the for loop runs the monadic IO action solely for effects.

The most useful definition of "effect" is a computation which alters its environment.

Here the the environment is the real world. The effect that the IO monad represents is an imperative operation which interacts with the external world through IO. So >> is basically the sequencing of IO operations in a way that discards the value yielded from executing some I/O operation. The only reason this I/O is being performed is for the side effects that will result.

Lets look at another example which runs through the actions in the list in a different way. While loops are loops that execute some block of code whilst a predicate is true.

    whileM_ :: IO Bool -> IO a -> IO ()
    whileM_ p f = do 
        x <- p
        if x then f >> whileM_ else return ()

Here p is an monadic computation which produces a value of type m Bool. This value is our predicate within the while loop. In other words, p is a monadic value which when executed yields a boolean value. The predicate determines if we continue or abort the loop. Interestingly, p is not the predicate itself. It is a IO action which when executed will produce a predicate. So p could be constructed from the IO value getLine we initially looked at which represents the operation of retrieving a string of characters from std input.

Oh wait what is this?

    unsafePerformIO :: IO a -> a 

This function doesn't make sense under our mental model. It defiantly blurs the distinction between evaluation and execution. Hence the words "unsafe". This function allows you to get the resulting value from an IO action by evaluating it. No execution in sight. This shouldn't be possible right?

So you might think at this point, are we just fooling ourselves by drawing this line if we can just ignore it like this?

Computer science like any science uses models which are simplified representations of the world.

So our criticism of this particular mental model of programs on the sole basis of the model being an inaccurate representation of reality is misdirected. The truth is that this is a flaw with all conceptual models. All conceptual models are simplified versions of reality so naturally there are always many to choose. Being aware of the fact there is always more than one model to choose from to understand a phenomena is important for programmers.

When you first peel back the layers of abstraction you might just say well everything is bits and bytes. Aren't we fooling ourselves? Natural sciences focus on studying things that already exist, atoms, cells. "Artificial" sciences like computer science shift the focus onto things that humans have created. In this way software engineering can employ insights from better models obtained from computer science. These better models have more explanatory power to allow us to understand how to manipulate information in a way to satisfy our goals.

Here is what Alan Kay wrote on the subject in a quora answer .

“Science” in its modern sense means trying to reconcile phenomena into models that are as explanatory and predictive as possible. There can be “Sciences of the Artificial” (see the important book by Herb Simon). One way to think of this is that if people (especially engineers) build bridges, then these present phenomena for scientists to understand by making models. The fun of this is that the science will almost always indicate new and better ways to make bridges, so friendly collegial relationships between scientists and engineers can really make progress.

Recently I actually read a tutorial which said that "Haskell programmers are fooled into thinking IO is pure". To this I respond with the following question.

Are we fooling ourselves when we draw a line between "stack" and the "heap" if to the CPU it is all just memory? For a C programmer this distinction is very real.

Well. Are we fooling ourselves into thinking these things aren't ultimately just an array of bytes as well?

Computer science isn't like other sciences. Natural sciences like chemistry use microscopes to study atoms. Chemistry creates theories which explain phenonemona in the physical world. It is the reverse in computer science. Computers are physical models of abstract processes. Physical machines have voltages which vary continously somewhere in between high or low for 1 or 0. We built these machines to implement an abstracted notion. I believe it is in this sense that Simon Herbert says, computing is a science of the artifical.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment