Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2016-05-20.
We discovered that
main is the entry-point of all Haskell programs. In other words, the point where they start executing. We also discovered that
main is an action, so now we’re going to delve a little bit into actions,
IO, and what purity means.
Bear with us while we work through this part. Haskell works differently than you would think, and it’s important to get a clear mental picture of how it behaves.
Haskell programs are made by writing definitions in text files, and running a program called a compiler on those text files. The compiler understands Haskell, and translates the text files as Haskell into an executable program, which we can then run.
In Haskell, all the things you can write are called pure: definitions, values, expressions and functions, even the values that produce actions.
Pure here means they are consistent, equational and express truths about value. That makes them easy to think and reason about. They’re expressed in the world of the computer, where pure thought can exist. All the expressions in Haskell use this purity, which makes writing software joyful because we can lock parts of our programs down into separated, predictable behaviour that almost always works as we think it will.
To understand this, let’s think about the way addition works. It never, ever changes. If you add the numbers 1 and 5, you will never get a different result than 6. Of course! This is what we mean by pure — we can talk about what is true or false in this “world” with some certainty. Contrast this to something from the real world: the current time. This is not a pure value because it is always changing.
Let's see a program that does this. Don't worry if you don't understand anything about it yet. We'll explain it in later chapters:
-- a program to -- add 1 and 5 main :: IO () main = print (1 + 5)
Every time you run the program above, it will return 6. It can never change, which is because it only uses simple pure functions and pure values to obtain its result. Contrast that to the following program, which uses pure IO actions that contain non-pure values:
-- a program to get and print -- the current time as seconds import Data.Time.Clock.POSIX main :: IO () main = getPOSIXTime >>= print
When we run this program, it takes the current time from
IO, and passes it to some screen-printing code which is built into the
IO portion of the
Something to note, though, is that
Some more about this. So how can we say everything in Haskell is pure, if we just showed you a Haskell program that deals with the time which we just explained is an impure value? Well, the Haskell program is pure, and only expresses pure values and functions, but the IO type allows us to describe and contain computations and values that deal with the non-pure real world!
Pure values and expressions and functions are much easier to reason about because they're simple and direct, which is why it’s nice to program in Haskell: It lets us talk about the non-pure world using pure descriptions.
Unfortunately, most of the interesting things we want to program are not completely pure. Haskell has certain special functions and values that are marked as
IO actions. You’ve seen one or two already. These are also pure, like everything in Haskell, but they give us a gateway into the messier “real world”. They let us describe actions that can happen in the real world.
In Haskell, the types of things that let us do this are made with a constructor named
IO, which stands for Input/Output. That is, the world of the error-prone, non-pure, complicated, real world where things are always changing, and where there is input and output from keyboards, mice, disks, time, the internet, random numbers and printers. The real world is usually hard to think about simply, and much more difficult to program for. It doesn’t obey the same rules as the pure world and is harder to lock down into predictable behaviour because it’s so complex and complicated.
getPOSIXTime :: IO POSIXTime
As we can see, the type of the function that can get the POSIX time (a type of time value) is an IO type, which shows that it is an action in the impure world. That means it will return a POSIXTime when it is executed.
IO actions can be executed, which is what happens when their
IO behaviour is activated - this is how programs are run. This
IO behaviour is effectively invisible to the pure world. The pure world can never “see outside” to the real
IO world, however the
IO world can use pure values, functions and expressions without problem. Running a program is how we execute
IO actions. When we compose our
IO actions with other
IO actions and pure functions, we can build up useful, functioning programs.
Let’s use an analogy here: think about a simple electric circuit: a battery, some wires, a switch and a light globe. By themselves, all of these things are simple, “pure” things. The light and switch are slightly different than the others, but they are still just themselves. However, the switch can “do input” into the circuit, and the light globe can “do output” from the circuit in the form of glowing light. The wire and battery can do no such things. They don’t have any action in the real world at all.
If we connect these components into a circuit by binding them together, we can throw the switch (that is, give the circuit some input) and the light bulb will cause there to be light (that is, we’ll get some output). This doesn’t change what these components are, obviously. However, some of the parts provide effects in the world outside the “world” of their pure value inside the circuit. The wires are like our totally pure non-
IO functions in Haskell because they don’t do anything outside the circuit: they’re not like the light or the switch.
You can see how all of these components are in themselves purely what they are, and all of them can be used as part of other bigger composed circuits, which in total have some
IO action on the real world, but also that some of these component (such as the wires) don’t actually do any actions in the world of
IO for the circuit, but are necessary for the whole circuit.
In a similar way,
IO actions have a foot in both worlds: while being able to be evaluated like ordinary Haskell expressions, which does nothing in the
IO world, they also contain the ability to be executed: that is, to interact with the outside world and effect it in some way, and so they are marked with the
IO type marker. To create programs that interact with the
IO world, we bind combinations of these
IO actions together along with pure functions to create bigger
IO actions that can do what we want.
Haskell is an awesome language: it’s capable of letting us cleanly think about and build pure expressions and functions, and also lets us connect these pure things up to the real, messy world in a way that keeps as much of our programs simple, clear and separate as possible.
When a program is composed of pieces like this, it’s very easy to reuse parts of it, and it’s much easier to spot problems and adjust things.
Note that Haskell programmers will often refer to a pure
IO value as an action, as well as referring to the part of one that effects the real world, such as the action of putting a message on the screen. This can sometimes be confusing, so just remember it can mean either.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.