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:
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:
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.
Here we use parenthesis to override the presedence of function
application. The above expression is equivalent to just writing:
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
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.
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
All this function does is multiply its two parameters. We can now use
this in conjunction with partial application to define this function
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:
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
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
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:
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:
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 nam
e 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?
- Read source code file name from the command line
argument.
- Compile the source code using GHC.
- 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!