The Lambda Complex
Tutorial INDEX
Books and papers
ABOUT this site
LINKS to other web sites
Introduction to Programming Applications in Haskell

Introduction
This tutorial series is targeted towards the imperative programming crowd - not the newbie programmer. However, Haskell is so different from other langauges that this probably won't matter much.
The only difference between these tutorials and a tutorial series aimed at total programming newbies is that I will aim to include much more detail about all topics we touch upon. So whereas a newbie tutorial would briefly touch a variety of subjects, and then return to them later, these tutorials will include much more information up front. The reason for this is that programmers who only get "part of the picture" will immediatly start to dream up situations where the small part they are given will not get the job done, and thus get frustrated.

Tutorial 1
In this tutorial we will go through the basics of programming applications in Haskell. First we will go through some of the basic data types. Then we will go through functions; how they work in Haskell, and things that are different from most imperative programming languages. We will then briefly discuss IO in Haskell, since this is a touchy subject with functional programming. In later tutorials we will not use much IO, but rather concentrate on other subjects - advanced IO topics will be handled later. We will also touch on the subject of software design in functional languages.

Haskell basics
First we shall state some basic information about Haskell that you will need to know when reading the following sections. First two important properties:
  • Haskell is lazy. This means that a function may use definitions that are defined after it, it also means that you can define infinite data structures - but we'll leave that alone for now
  • Haskell is layout dependent. The scope of a function is defined by the layout. Picture drawing a box aroud each function definition. Anything within this box will be within the scope of that function. This way there's no need for brackets to determine which statement belongs to what. Also, line breaks are considered to be terminators (no need for semi-colons):
On a design note you'll notice that functional languages make use of recursion to a much greater extent than imperative languages. There are no for-loops in Haskell! Things like that belong to sequencing, imperative, languages.
To, say, sum the values from 1 to 10 you would probably use a for-loop in an imperative languages in conjunction with a mutable variable. In Haskell you would probably generate a list with elements from 1 to 10 (seems like  heavy operation, but all you do is write [1..10]) and then recursively add the first element to the sum of the rest of the list until you reach the empty list, which has a sum of 0.

Haskell types
Haskell comes with a wide variety of basic data types (as well as the ability to define your own). Int, Double and String are the most common ones, although there are more. I will assume you know what these types represents, so I won't go into it more.

Lists
Lists is a standard data type in Haskell (for the curious, they are implemented as linked lists internally). The nil list, or the "empty list", is written []. A list can be constructed using the "cons" operator, : , which will add an element to the front of an already existing list; for example 1:2:[] is the list of 1 and 2. There's also a shorthand for constructing lists that looks like so: [1,2,3,4], which is the same as writing: 1:2:3:4:[].
A list can be constructed from any type (even functions or user defined types!), but all elements must be of the same type.
Lists are very important in Haskell, any operation using sequences (like the "sum of numbers from 1 to 10" example above) should be programmed using lists. Because of this there are shorthand notations for generating some simple sequences. To generate the list from 1 to 10 we write [1..10], to generate the list with every other number from 1 to 100, you would write [1,3..100], the first two elements give the "step" of the sequence.

Tuples
In addition to providing basic types, Haskell provides a convenient way to group types together into tuples. For instance (2,4) is the tuple of the Ints 2 and 4. This is extremly useful, grouping values together is a very common pattern (you want to return two values from a function in C? Tough luck! You'll have to resort to pointers or other nastiness (like declaring and initializing a struct) - in Haskell you just return a tuple of two data types which can be created in place, there's no need to name it - it's just another value).
Tuples aren't restricted to just two elements, (1,2,3) is the 3-tuple of the Ints 1,2 and 3.
Any data type can be put inside a tuple, even the ones you define yourself. Also, the contents of the tuple can be of differing types: ("Haskell", [1,2,3], 4.5). Note that a list is also considered a type and can thus be put inside a tuple - this works the other way as well, you can have a list of tuples.


Functions in Haskell
Functions are a very important part of Haskell programs. In fact Haskell programs are mainly function definitions.

What is a function
A function takes a set of inputs, and returns a set of output. A program in Haskell is just a single function. A game, for example, could take mouse clicks as its input, and produce graphics and sounds as its output.
A function can in turn be defined by more functions, which can be defined in terms of even more functions.
A very central concept is that the result of a function is only determined by its input. There can be no mutable variables that would cause a function to behave differently with respect to time, for instance.
A function in Haskell is also a value. You can pass a function as a parameter to another function, for instance. A function can also be returned by another function. You can even put a function in a list, or any other data construct.

Functions in Haskell are defined like so:

f a b c = a+b+c

First comes the function name, f, and then the parameters a, b and c. On the right hand side of the equation comes the expression which the function will evaluate, in this case its just the sum of the three parameters.
To call a function we do like so:

g = f 1 2 3

Here we define the function g to be the sum of 1, 2 and 3. Now you could consider g to be a variable, but it's probably more consistent to think of it as a function with zero parameters that returns the expression on the right hand side.
Because the output of a function is only determined by its input you can apply a function by replacing its name by the expression in the right hand side of the function definition, and the parameter names by the actual values.
Notice that we use no paranthesis in function calls. Parenthesis is used for grouping only.

h = f (1+2) (2+3) (3+4)

Here we use parenthesis to override the presedence of function application. The above expression is equivalent to just writing:

h = f 3 5 7


Partial application
Now, the function f above is a function with three parameters, but that's not the whole truth. Consider this function, which we define in terms of f

j a b = f 5 a b

The function j takes two parameters which are added together with the integer 5.
Now we will take a look at an alternate definition of this function.

i = f 5 

This function is completely equivalent with the function j above. So how can that be? We state that the function i is equal to the function f passed only a single parameter, 5. But f was supposed to take three parameters, surely this must be a programming error?
It isn't. This is completely valid Haskell syntax. What this means is that you apply 5 to f, the result of this is a function that takes the two remaining parameters.
You can think of it as f "waiting" for the rest of the parameters. Thus i will be a function of two parameters, the two parameters that f is "waiting" for.

A nice way to think of this is that all functions are really just a function of zero or a single parameter. The function f a b c is the function that takes one parameter, a, and returns a function that takes the remaining two parameters, b and c. This function is in turn a function that also takes a single parameter, b, and returns a function that takes the last parameter, c. This takes a single parameter, c, and returns the function of zero parameters that returns the value of a+b+c.

Applying a function to only some of its parameters is called partial application and is one of the most important concepts you'll need to understand in Haskell.
Consider the (somewhat useless) function

mul a b = a*b 

All this function does is multiply its two parameters. We can now use this in conjunction with partial application to define this function

double = mul 2 


Think about what happens in the right hand side of this equation. We pass 2 to the function mul. Now 2 is bound to the variable a in the definition of mul. This means that mul is "waiting" for the value of b. In other words; the result of mul 2 is a function taking the second parameter and multiplying it with two. So since the value of the right hand side is a function of one parameter, double must also be a function of one parameter. We can write this explicitly if we wish:

double x = mul 2 x 

This is equivalent to the definition using partial application, but you should try to get a feeling for partial application since this will likely help you get a feeling for Haskell in general. This type of flexible playing with functions is central to the expressiveness of Haskell. A function is no longer a monolithic black box that can not be altered, it's a value just like all other values, and it can be modified just like values can (although you can not "update" a function name to mean something else since this would break the "no side effects" rule, and thus the purity of Haskell).


Type signatures
Notice that we have not written out any type signatures on any of the functions above. This isn't needed, Haskell will infer it automatically (and a common pattern is to omitt type signatures and then load the function into an interpreter, which will infer a type signature which can then be copied and pasted into the source code). There is a facility to define the type of a function, however. The function mul above can be defined to work on ints like so

mul :: Int -> Int -> Int
mul a b = a*b

(Note that this definition of mul will only work on ints, whereas the version where we omitted the type signature will work for any type which as the * operator defined. We will go into polymorphism in greater depth later).

Take a look at the type signature. What it says is just "A function from Int to Int to Int". Notice that there is no clear distinction which is the output and which is the input. Normally you consider the last part of the type signature to denote the type of the output, but if you take partial application into account you could just as well consider the function mul to be "a function that goes from an Int to a function that goes from Int to Int". In other words the output of the function is another function ("from Int to Int").

mul :: Int -> (Int -> Int)
mul a b = a*b

This highlights that the function takes one parameter and returns a function (which "goes from Int to Int"). But note that this type signature is completely equivalent to the other type signature.

The type signature of a "variable" (or a function with zero parameters) is just

a :: String
a = "Hello"

We just say that a is of type String (no arrows).

We can also define type signatures for functions that use tuples and lists:

listFunc :: [Int] -> [Int]
tupleFUnc :: (Int, String) -> (Int,String)
listTupleFunc :: (Int,String) -> ([Int],Double)

Here we have omitted the function definitions and just written down some example type signatures. The first one takes a list of Ints and returns a List of Ints. The second one takes a tuple of an Int and a String and returns a tuple of an Int and a String. The last one is a bit more refined and takes a tuple of an Int and a String, returning a tuple of a list of Ints and a Double.

Type signatures are, as we already stated, not necessary in most cases. But it's considered good style to include them anyway, as a way of code documentation.

Higher Order Functions
Higher order functions is a name for functions that take another function as a parameter. This is not "just another feature". This is very important, one could argue that it is the whole point of Haskell. It does take some getting used to so we will just give a few quick examples, for now. But we will revisit this topic many times in the following tutorials.

twice :: (Int -> Int) -> Int -> Int
twice f value = f (f value)

As we can see from the type signature this function will take a function of type "Int to Int" as a parameter, and then a value of type Int, then it will return a value of type Int.
We can see from the definition that it will apply the function (which we assign the name f ), and then apply the function to the result.

Let's go on a side track briefly here. The pattern seen above, f ( g x), in other words applying a function to some value, and then another function to the result is called "function composition" and is a very common operation in programming. In mathematics we have the "ring" operator to do this, in Haskell we use the . operator for the same result. Thus we can redefine twice like so:

twice :: (Int -> Int) -> Int -> Int
twice f = f . f

Note the partial application again. The . operater will apply the right most function to the parameter, and then the left most function to the result. So this function definition is equivalent to the previous one.

The beauty of the twice function is that it can be used to double the effect of any function. For example:

quadruple :: Int -> Int
quadruple = twice double

Note the partial application at work here. Twice is only given the first of its parameters (the function) so it will "wait" for the Int. In other words the result of twice double is a function taking the remaining parameter.
Go through the steps of applying this function quadruple to some value of type Int by hand.


Pattern Matching and Recursion
Another important feature is pattern matching. This allow us to write several definitions of functions for various patterns. Let's define a recursive factorial function in Haskell:

fac :: Int -> Int
fac 0 = 1
fac n = fac (n-1) * n

Okay, so first we have our standard type signature. The next row is a function definition, but notice we haven't named the parameters, we just put a value there. So when we pass a value to the function Haskell will try to assign that value to whatever is in the place of the parameter. Now you can't very well assign anyting but 0 to the value of 0 so if we try to pass anything but the value 0, the pattern match will "fail" and Haskell will continue down to try to find a pattern that matches.
Note that Haskell will always try the pattern that is declared on top first, then the second one from the top and so on.
So when we pass 0 to the function the pattern will match, and the result is just 1. If we pass any other value, say 2, the pattern will fail, and Haskell will move on to the next definition. Here it will try to assign 2 to  the parameter name n which will work since can be any value (as long as its an Int), so the result is fac (n-1) * n, in other words it will pass 1 to the fac function and multiply it by n.

So this will recursively compute the faculty of a number. We reduce n by one each time until the parameter eventually matches the first pattern, 0, and returns 1.

Let's take a look at a slightly more difficult example of pattern matching, higher order functions and lists. We define the function map

map :: (Int -> Int) -> [Int] -> [Int]
map f [] = []
map f (x:xs) = f x : map f xs

First note the type signature. The first parameter is a function from Int to Int. The second parameter is a list of Ints, and the result is a list of Ints. The purpose of this function is to apply a function to all elements of a list, and return the resulting list.
The first pattern matches the function against f, which will always match, and the list against [] which will only match if the list is equal to the empty list. If this is the case we are trying to apply a function to all elements of an empty list, so the result should be just an empty list.
The second pattern is a bit more complicated. The function is matched against f again, and the list is matched against (x:xs). Remember that : is the "add to front of list" operator so this pattern must mean "the head is x and the rest of the list is xs". Thus we have effectively named the contents of a list, which we can then use in the expression on the right hand side.
So now we have the head in x, we apply the function f to this element, and append it to the list map f xs which is the function f mapped onto the tail of the list.
This is also a recursive function. Each step we will apply the function f to the first element and keep going until we reach the empty list (xs needs only be a list, so it can be the empty list as well as any other list) in which case the recursion stops.

Let's follow the execution of the statement map double [1,2], which is the same as map double (1:2:[]). Paranthesis are required because functions bind more tightly than the "cons" operator.
Now first we will test [1,2] against[] which will obviously fail, so we will move on to the next pattern, which will assign x to the head of the list, 1, and xs to the tail of the list, (2:[]) or [2]. The expression will then be: f 1 : map double (2:[]).
We follow this another time and see that (2:[]) will fail the test against [] and be passed to the second equation, now x will be 2, and xs will be []. We now have: f 1 : f 2 : map double ([]).
If we apply map to its parameter again we will see that it now matches the pattern [] and returns []. So now we have f 1 : f 2 : [], and if we substitute f for the function double which we have passed into map we get: double 1 : double 2 : [] which, if we apply the function calls, gives us: 2:4:[] or [2,4] which is exactly what we would expect to get from doubling each element in the list [1,2].

It just so happens that the function map is so useful that it is actually defined in the Prelude, the default module that is automatically loaded into any Haskell program.

Let's take a final example. To solve the problem of summing the values from 1 to 10 mentioned earlier we could do the following.

sum :: [Int] -> Int
sum [] = 0
sum (x:xs) = x + sum xs

The sum of an empty list is just 0, so there's the base case. The sum of the list with the head x and the tail xs is just the value of x plus the sum of the list xs. We can now use this function to compute the sum of all values from 1 to 10

sum [1..10]

Let's generalise this function a bit, using higher order functions. We can parameterize the function which is used on the list to combine the elements of the list. So we'll need to pass the function (which was +, in the case of sum) and the base case (which was 0 in the case of sum).

foldr :: (Int -> Int -> Int) -> Int -> [Int] -> Int
foldr func base [] = base
foldr func base (x:xs) = x `func` (foldr func base xs)


This function is named foldr, meaning "fold right". What this does is take some function, func, and apply it to each element and the rest of the list ("folding it", into the list), exactly like we use + in the sum function. The base value is the identity value of the function, for the sum function this was 0. It's the value which is returned for the empty list.
There's a bit of new syntax here. By enclosing a function of two parameters like so `func` we can actually make that function an infix operator (like + and *). The reverse is also possible, to transform an infix operator you enclose it in paranthesis (for example you could write (+) 4 3, to add 3 and 4, just as well as 4 + 3).

Take time to understand this function as it is very useful, and also typical for programming in Haskell. It may help to understand it by simply looking at the similarities to the sum function.
Now let's redefine the sum function using our new foldr function.

sum :: [Int] -> Int
sum = foldr (+) 0

Now we use the "infix to postfix" feature here. By enclosing the + sign in paranthesis we transform it to a regular two parameter function which we then send off to foldr. The identity case for this function is 0 (the sum of an empty list is 0) so we send that as the second parameter. Now there's some partial application at work here as well, foldr expects a list as the last parameter, so sum will be a function expecting a list and returning an Int.
One way to comprehend the foldr function is to think of it as replacing the : operator in the definition of a list, and the [] with the passed identity case. So if we write out the list [1,2,3] to 1:2:3:[] and replace each : with + and the [] with 0, we get 1+2+3+0 which is the expected result.

Let's define another function, product, using foldr

product :: [Int] -> Int
product = foldr (*) 1

This will produce the product of a list. Note that we use 1 as the identity case here - we can't use 0 since that is not the identity of multiplication.
Write out this function on an arbitrary list, and convince yourself that it produces the correct result.

Finally, we will rewrite the fac function using our new product function.

fac :: Int -> Int
fac n = product [1..n]

So the since the faculty of a number nis the product of all numbers from 1 to n we can use lists together with our product function to cleanly define the faculty function.

So we have now seen that foldr is quite useful, so useful that it too is defined in the Prelude, although it is then defined polymorphically (it works for all data types, not just Int) - we can do this easily by simply removing the type signature (Haskell will then figure out the most general type signature automatically). So is sum and product, by the way.

This whole section should have given you an idea of the power of higher order functions. We defined one very general function and used it to define many other more specialied ones. Thus, if we can prove that the general function is correct, we will have several proven functions for "free".

Lambda Functions
There is an alternate way of defining functions that can be very useful when you wish to construct a function on the fly. It's called "lambda functions" and it looks like this:

\a b c -> expr

The starting backslash is meant to look like a lambda symbol (from this site's logo, for instance). The a b, and c are the parameters, then there's an arrow and after that the expression.
The advantage of this notation is that you can create unnamed functions which can come in handy when you wish to map some operation on a list, for instance:

func xs b = map (\a -> 2*a-b) xs

Here we create a function on the fly and send it off to the map function. So for small functions this can be very convenient since we don't need to name it. Of course if one were so inclined we could use this to define any function:

double = \a -> 2*a

Here we name a function created by using the lambda syntax. Most people prefer to use lambda functions only occasionally (like for use with map, filter, or foldr), and stick to the regular function syntax for everyting else.


The where notation
There is one more thing I would like to go through on this topic. What if we have a function definition that won't fit on a single line? We can use the where-notation to rewrite the map function above.

map :: (Int -> Int) -> [Int] -> [Int]
map f [] = []
map f (x:xs) = f x : mapped
where mapped = map f xs

Basically you can put a where clause underneath a function and make definitions there which can be used in the function itself. These definitions are in the same scope as the function itself and as such it has access to the parameters of the function.
In this short definition the where clause really wasn't called for, but for larger function it is very useful to make the definition less cluttered.

There is more to functions than this, but the rest will be handled as we run into it later in the tutorials. The topics we have discussed so far should be enough to give you a basic understanding of writing Haskell programs.

That pesky issue of IO
Remember when we said that Haskell was pure, and didn't allow any side effects? Let's think about this. Suppose we have a function that will read from a file and return the contents. Then this function clearly wouldn't follow the "Output from Input" rule!
My oh my, we seem to have ourselves a little predicament! It seems that IO is impossible in a purely functional language! Or is it?

Managing IO Without Breaking Purity
The solution to problem is to introcude an abstract concept of a world. Whenever you perform an IO action you pass this world as a parameter and not only do you receive the value of this action, but also the new, modified world. This world can then be used to perform another IO action. You can not
use a world from a previous IO action later in the program. That world is invalidated as soon as an IO action is performed on it.
Don't worry, all this fuss about the world is completely abstract, the programmer never sees it.

Now, all of this passing of the world will actually make IO fit into our purely functional model. If the world is passed to the function as a parameter then the function is still determined only by its input!
To prevent people from accessing this abstract world this is all encapsulated into the IO monad.
Briefly we can say that a monad is a datatype representing some form of computation and two functions. One of the functions is the bind function which will take two computations represented by the monad and sequence them, one after the other, producing a new computation equivalent to the sequencing of the two previous computations. The other is a function which will take any value and turn it into a value of the monad type (a constructor if you will).
The IO monad has type IO a, meaning that it is of type IO and then a polymorphic type. IO Int, IO String, and IO (String,[Int]) are all examples of IO types.

The big thing about monads is that we are able to hide the passing of the "world" (so the programmer need not worry about it, and there is no risk of using an invalid world) while also giving us the ability to sequence computations (actions). It is important to note that this fuss about monads and sequencing is actually all perfectly legal Haskell (there are several other monads besides the IO monad), they're not any language extensions.
In other words, the IO monad allows us to perform IO in a pure language by pretending that there's a world passed between the IO actions.

The key thing about the IO type is that it's "sticky". There's no way to extract the value of the 'a'-part of the datatype outside of an IO action.
You can
name the contents of a value of the IO type from within a sequence of IO actions. But there is no way to get the value of the computation from an IO type outside an IO action.
This means that you can not call an IO function (an IO action) from a non-IO function (since that would give you a result of type IO which can only be used inside another IO function). You can
call "pure" non-IO functions from an IO function though, and that is how Haskell programs are written.

Haskell programs thus have an "impure" layer between the user and the program, this layer is an IO Action, from this action calls will be made to pure functional non-IO functions.

Haskell thus have a clean separation between "program" and "outside world" via an IO action. This allows us to maintain the purity of Haskell, while still reading input from, and presenting output to, the outside world. Here's the standard Hello World program in Haskell.

main = do 	putStrLn "Enter your name:"
name <- getLine
putStrLn ("Hello " ++ name)

This program will ask the user for his/her name, and then print a greeting on the screen.
Notice how the syntax for the "impure" part of Haskell looks very much like an imperative langauge. The first function putStrLn has the type IO () meaning that it performs an IO Action, but doesn't return any result (it returns an empty tuple!). The next function getLine has the type IO String meaning that it performs an IO Action and returns a String. Now the <- operator will assign a name to the String (so the variable name will be of type String, and not IO String) this is then used in conjunction with the putStrLn function again (the ++ operator concatenates two lists, and a String is just a list of characters). The putStrLn function returns IO () which is also the type of main (which must exist in all standalone Haskell programs).

So the typical Haskell program will have a small IO layer written in a very imperative style. This layer will then call the heart of the program which is written in a standard functional style (as seen thus far). We can say that Haskell programs have an imperative shell, and a functional core like illustrated below.



In some of the small example programs it might appear as though the main part of the program is actually written in imperative style, but I can assure you that's not the case. For the purposes of brevity we won't often go through large code examples here, but in the "real world" the only IO actions used is the main function and a small set of helper functions, the rest of the program is written normally in functional style.
One should strive to have the imperative part of the program as small as possible. If you wish to write imperative programs, you're better off using an imperative language!

A Real World IO Example
We will now look at a real world program consisting only of a single IO action (the main function). As such it is not representative of most Haskell programs, but should give you some insight to IO-programming as it uses almost all the things you need when programming IO actions.

The program UltraEdit (which I and some others use to program Haskell code while on windows) has support for defining your own tools. To simplify the task of compiling our Haskell programs we will construct a program that takes the name of a Haskell source code file on the command line and then compiles it with GHC, if everything compiles correctly the resulting program shall be run.
UltraEdit can then be set up to run the "tool" with the following settings in the tools dialog:
Program path: dir/CompileAndRun.exe %N
Working dir: %P
Windows program: checked
The %N command tells UltraEdit to supply the filename (excluding extensions), the %P command inserts the path to the file.

So what should our program do?
  1. Read source code file name from the command line argument.
  2. Compile the source code using GHC.
  3. If the compilation was successful: run the program.
Here's the Haskell code which does just this:

import System

main = do (srcName:_) <- getArgs -- get arguments
let fname = srcName ++ ".exe"
exitCode <- system $ -- compile source code
"ghc -o " ++ fname ++ " --make -O2 " ++ (srcName++".hs")
case exitCode of
ExitFailure _ -> putStrLn "Compilation failed!"
ExitSuccess -> do putStrLn $ "Executing " ++ fname
system fname -- run program
return ()
putStrLn "\nPress ENTER to continue"
getLine


Let's go though this piece of code one step at a time.
First we import the System module which allows us to execute programs using the system function.
The first thing we do is initiate a do-clause which will allow us to sequence IO actions one after the other while optionally assigning names to the values of the computations. Note that the do-clause is just syntactic sugar for the use of the bind-function. We're really just merging commands two at a time into a single IO action.
The first row uses the function getArgs which returns a list of command line arguments to retrieve the source code file name. Notice the patternmatching. We know that we only need the first argument so we patternmatch against that and use _ to signifiy that we do not care about the rest of the pattern.

Next there's some new syntax. We will now use the string we extracted on the previous line to build the file name for the executable file. We do this by using a let statement, which simply assigns a name to a value. We do not use the <- here because the right hand side is not an IO action, but a regular expression.

After this we call the function system using the compiler syntax for GHC (making the file to the supplied name, fname, and optimizing using the -O2 flag). The result is bound to the name exitCode. We have introduced the function application operator $ here. This operator is designed to reduce the number of needed paranthesis. It is equivalent to inserting an opening bracket, and then a closing bracket at the very end of the expression. Thus f $ g $ h is equivalent to f ( g ( h )).

After this we wish to examine the exitcode of GHC to determine whether the compilation was successful or not. We do this using the case statement. This language construct doesn't work exactly like in other languages. It allows us to evaluate an expression and then pattern match against the result. Each pattern is given a result which will also be the result of the whole case statement if that pattern should match.

If the value of exitCode matches against the constructor ExitFailure (note the usage of _ to signify that we don't care what the exit code was, just that it failed) then the result of the case statement is the result of printing "Compilation failed!" (which is just IO ()).
Note: A constructor is a function which will construct a data type. The constructor ExitFailure Int will construct a value of type ExitCode from an Int. We can pattern match against type constructors by simply replacing the parameters of the constructor with variable names (or _ if we don't care about the contents of the type) and then proceeding as usual.

If the value of exitCode matches against the constructor ExitSuccess then we initiate a new sequence of IO actions which will execute the program using the same function as before.
Note that since the result of the case clause is supposed to be a single IO action, we must start a new do-clause to sequence several IO actions into a single IO action.
View the do-clause as something which will "merge" several IO actions into just one (returning the value of the last IO action in the sequence).
At the end of this new sequence we put return () to make sure the type of the case clause is always the same. The type of the first case was IO () so the type of the second clause must also be of that type, but system returns a value of type IO ExitCode so we must add another function which does nothing but return the value it is passed wrapped up into the IO monad, this function is return. Note that return does not mean "end function" like in other languages. It simply means "make an IO value of this value". So return () will simply give us the value IO (), while return 4 would give us IO 4 (of type IO Int).

After the case statement we output a goodbye message and then we wait for the user to press enter before the main function, and thus the program, ends.

So what's the deal with all this IO business? Why should we bother with Haskell if we end up writing imperative code anyway?
Well, first of all most code will not be inside IO actions. Secondly no other imperative language does a better job at it! Haskells IO-system may be imperative, but so is the IO-system of all imperative langauges! Dealing with 1% imperative code is better than 100% imperative code!

Conclusion
Hopefully you will now have a basic understanding of how Haskell programs work. Join me in the rest of this tutorial series wich will cover several case-studies, data types and more!