Blog

Haskell Crash Course

Getting Started

You can download Haskell here.

You can often also install it by installing the haskell-platform package with your favorite package manager.

The Interactive Intepreter

GHC (the Glasgow Haskell Compiler) comes with an interactive interpreter. You start the interpreter by running ghci at the command line.

The code you write at the interpreter has very similar syntax to regular Haskell code. The biggest differences are that you start function definitions with let and you have to split up multi-line statements with semicolons instead of newlines.

To follow along, I recommend starting GHCi now.

Once in GHCi, run the following:

:set prompt "> "

That changes GHCi's prompt to be less clutter-y.

Values and their Types

Try entering

5

into GHCi. You will see that it prints the number "5".

When you type a number into GHCi, it will interpret that number as a Float, Integer, Int32, etc. depending on the context. By default, GHCi interprets numbers as Integers.

We can make it clear from the context that GHCi should interpret the number as a float. Try typing

5 :: Float

<Value> :: <Type> means "Value has type Type", so we're telling GHCi that 5 is a float here.

As you can see, GHCi now prints "5.0" instead of "5".

Now let's try assigning that value to a variable.

let x = 5 :: Float

Now, if you enter

x

GHCi will print "5.0".

If we don't know the type of something, we can ask GHCi. Try typing

:type x

GHCi should tell you that x :: Float (meaning, again, that x is a Float).

Numbers

Haskell has some built-in number types. The most common ones are Float, Double, Int, and Integer. Int is fixed-size (usually 64-bit) while Integer is arbitrary-precision (like Java's BigInteger).

There are also unsigned int types available in the Data.Word package.

In some languages, you can add numbers of different types and the language will implicitly cast those numbers to the same type before adding them. Not so in Haskell. You can only add/subtract/multiply/etc. numbers of the same type.

For example, try running

(1 :: Float) + (1 :: Integer)

You will get an error.

Remember how I said that, if we type a number into GHCi, it will guess the type of the number based on the context? Let's test that out.

let y = x + 2

Remember that x :: Float. Let's ask GHCi the type of y.

:type y

As you might have guessed, y :: Float as well. That means that GHCi recognized from the fact that x :: Float that 2 must be interpreted as a Float as well. Of course, the result of adding two Floats is another Float.

We have a number of functions to convert between number types. Probably the most useful ones are fromInteger and toInteger. fromInteger converts an integer to any other type of number, and toInteger converts whole numbers (Int, Int32, etc.) to Integers.

Functions

Let's try writing a function on a number.

let double x = x * (2 :: Int)

You may be confused by this syntax; Haskell doesn't put parenthesis around function arguments. You just write them after the function name, separated by spaces.

Try running

double 5

As you may have guessed, the result is 10. If we try again with

double 5.0

we get an error, because we're trying to multiply a Float by an Int.

Let's try checking the type of double.

:type double

GHCi tells us double :: Int -> Int, which means "double takes an Int and returns an Int".

Let's try writing a function to square a number.

square x = x * x

If we square an Int, it should return an Int, and if we square a Float, it should return a Float. Let's try it:

square 5
square 5.0

As you can see, square 5 returns 25, while square 5.0 returns 25.0.

Let's check the type of square.

:type square

GHCi says square :: Num a => a -> a. Notice the thick arrow and the thin arrow. This means "square takes an a and returns an a, as long as a is a Num". In other words, this function isn't restricted to one particular type of number; it will take and return any type of number.

Note that square won't ever take an Int and return a Float; it will always return the same type of number that it takes. It is simply capable of taking (and returning) different types of numbers depending on the context.

We can also write functions using lambda syntax. To implement square using lambdas, we write it as

let square = \x -> x * x

This does exactly the same thing as the earlier implementation. Lambdas are useful because they let us implement functions without naming them. We'll see how that can make our programs simpler and shorter in a little bit.

Lists

Lists are one of the most fundamental data structures in Haskell.

Haskell Lists are distinct from C or Java arrays, Python lists, etc. Because Haskell is a pure functional language, it doesn't allow us (generally speaking) to change the value of a variable after it's been assigned. Therefore, if we wanted to update a single element in a C-style array, we'd have to copy the whole array (which would obviously be very slow).

Haskell Lists are linked-list data structures. That is, each list element is a piece of data and a pointer to the rest of the list. An empty list is represented by [].

Let's do some stuff with lists.

let nums = 1 : (2 : (3 : []))

: (pronounced "Cons") takes a piece of data (on the left-hand side) and a list (on the right-hand side) and turns it into a linked-list element.

This is exactly the same as writing

let nums = [1,2,3]

People generally use the latter syntax. Try printing out nums.

nums

GHCi uses the latter representation and prints "[1,2,3]"

Try this:

0 : nums

As you can see, this simply prepends 0 to the front of the list. Note that, because this is a Linked List, prepending (and popping) are very fast, as opposed to in most languages. The downside of using Linked Lists is that indexing is very slow, so we have to use other data structures for that.

Thankfully, because of Haskell's functional style, it's very easy for us to do almost everything without indexing. For example, let's say we wanted to double all the numbers in a list. Instead of having a for loop and multiplying each number by two, we can use map.

let double x = x * 2
map double [1,2,3]

As you can see, the result of this is [2,4,6]. Note that map doesn't modify the input list; it creates a brand new list with the map operation applied.

We can re-write this to be a lot shorter using lambda functions.

map (\x -> x * 2) [1,2,3]

Because we only use this "doubling" function once, we don't really need to give it a name, so we can eliminate a line of code by writing it as a lambda function.

Haskell has a list enumeration syntax that's pretty useful for generating sequential lists of things.

let positiveInts = [1..]
let firstTenPositive = [1..10]
let firstTenEven = [2,4..20]
let alphabet = ['a'..'z']

Haskell has list comprehensions. If you've done some Python programming, you'll be familiar with these.

let columns = ['a'..'f']
let rows = [1..6]
let coords = [(column, row) | column <- columns, row <- rows]

Haskell has a few commonly used built-in functions on lists.

head [1,2,3] = 1
tail [1,2,3] = [2,3]
take 2 [7,8,9] = [7,8]
drop 2 [7,8,9] = [9]
sum [2,3,4] = 9
product [2,3,4] = 24
zip [1,2,3] "abc" = [(1,'a'),(2,'b'),(3,'c')]
unzip [(1,'a'),(2,'b'),(3,'c')] = ([1,2,3],"abc")
filter (/= 2) [1,2,3] = [1,3]

Strings

Haskell built-in Strings are simply lists of Chars. We can even write

'H' : "ello World"

to get "Hello World". You can do anything to a String that you can do to any other list, including mapping.

Some useful functions that work on Strings (and lists) are (++) :: [a] -> [a] -> [a], for concatenation, and length :: [a] -> Int for length.

For example,

length ("Hello" ++ " " ++ "World")

Partial Application

Now, you may still be wondering why Haskell uses the syntax

plus a b = a + b

instead of

plustuple(a,b) = a + b

You actually can write it the second way. The type in that case is plustuple :: Num a => (a, a) -> a

That is, it takes a tuple of two numbers and returns another number.

Why, then do we choose to write functions in the style of plus :: Num a => a -> a -> a?

This decision has its mathematical basis in currying, but it has some very simple practical benefits.

Note that we can re-interpret the type of plus as Num a => a -> (a -> a). This means that, instead of thinking of plus as a function that "takes two numbers of type a and returns another number", we can think of it as a function that "takes a number of type a and returns a function of type a -> a". So we can re-write

plus 5 6

as

(plus 5) 6

These mean exactly the same thing.

As you can see, if we only give it one argument (5), plus simply returns another function that we can apply to the second argument (6).

Let's try assigning (plus 5) to a variable.

let plus5 = plus 5
:type plus5

As you can see, plus5 :: Num a => a -> a. We can then type

plus5 10

to add 5 to 10.

The nice thing about this is that we only need to apply as many arguments as we want; we're not obligated to fill out all the arguments. On the other hand, if we were using tuples for arguments, we would have to fill out all the arguments at once.

To see how this can be useful in practice, let's look at the type of map.

:type map

map :: (a -> b) -> [a] -> [b]. In other words, it takes a function from a to b and a list of as, and it returns a list of bs.

Or, alternatively, we can interpret this as map :: (a -> b) -> ([a] -> [b]). That is, we can use map to turn a function from a to b into a function from a list of as to a list of bs. For example,

let doubleItems = map (\x -> x * 2)

Because we only filled out the first argument of map, we now have a function that takes a list as an argument. You can verify this by checking that the type of doubleItems is doubleItems :: Num a => [a] -> [a]

We could also write this as

let doubleItems list = map (\x -> x * 2) list

but this adds unnecessary clutter.

Now we know that we don't have to fill in all the arguments for a function. But aren't mathematical operators (like *) just functions? Let's try it:

let double = (2*)

It worked! We can partially apply mathematical operators. We just have to put them in parenthesis so the compiler doesn't get confused. In fact, we can partially apply them on either side, so (*2) is fine too.

Let's re-write our doubleItems function:

let doubleItems = map (*2)

That's exactly the same as the (much longer and more cluttered)

let doubleItems list = map (\x -> x * 2) list

Infix Functions

Sometimes, it's nice to make an infix operator (like +) into a regular function (like add2). We can do this by putting the operator in parenthesis.

let six = (+) 2 4

We can also make a regular function into an infix operator by putting backticks around it.

let plus a b = a + b
let six = 2 `plus` 4

See if you can work out (using your knowledge of partial application) why this works:

let plus = (+)

Modules

Haskell distributes libraries in the form of modules.

There are some useful functions on []s that aren't included in the default imports. (FYI, all the default imports are just things from the Prelude module.)

An example of these functions is nub, which removes duplicates from a list. For example, nub "hello world" is "helo wrd". We can import it like this:

import Data.List
nub "hello world"

Or, if we only want to import specific functions from the module:

import Data.List (nub)
nub "hello world"

Or, if we want to import everything except nub,

import Data.List hiding (nub)

Or, if we want to import things so that we have to reference them by library name:

import qualified Data.List as L
L.nub "hello world"

If you'd like more info on a module, you can read the docs or use GHCi's :browse command.

Moving away from GHCi

GHCi is really great for learning something or trying things out, but when we want to write an actual program, we're going to want to compile it.

First, make a file called "Program.hs". It doesn't really matter what you call it as long as the extension is "hs".

If we want to compile this into a program, we need to define "main" just like in C or Java.

Because we're not using GHCi anymore, we don't have to put let in front of functions anymore. Put this in your ".hs" file:

double = (*2)
main = print (double 5)

Note that main is just a regular variable, like double. We'll talk about its type later.

To compile, run ghc Program.hs, and to run the program, type ./Program.

Your program should print 10 and exit.

Custom Data Types

So far we've worked with built-in types like [] and Integer. How do we make our own types?

Simple! We use a data declaration. Let's say we want to make a type that held three Ints and a String. We can do it like this:

data MyType = ThreeInts Int Int Int String

MyType is the name of the type (so if we wanted to write a function on it, that's what we'd put in the type signature). ThreeInts is the "constructor" for that type, which is how we assemble the three Ints and the String into a MyType.

We can make a MyType like this:

threeFives :: MyType
threeFives = ThreeInts 5 5 5 "It's just three fives!"

Now how do we actually do anything with these values of MyType? We have to get the contained Ints and String out somehow!

Well, one way is with "pattern matching". That works like this:

thirdInt :: MyType -> Int
thirdInt (ThreeInts first second third string) = third

It takes all the values contained in the constructor and assigns them a name, so we can do things with them.

Since we're only using one out of the four contained values, we can shorten our function (and make its purpose clearer) by replacing the unused values with underscores. For example,

thirdInt :: MyType -> Int
thirdInt (ThreeInts _ _ third _) = third

Now, it gets pretty annoying to write out functions to extract a value from a record for every single custom data type you make. Thankfully, there's a way to automate this a bit. If we change our declaration to

data MyType = ThreeInts {firstInt :: Int,
                         secondInt :: Int,
                         thirdInt :: Int,
                         string :: String}

It makes a function thirdInt that does exactly the same thing as the one we defined manually.

We can also make a new record with a field modified using this syntax:

threeFives = ThreeInts 5 5 5 "It's just three fives!"
twoFives = threeFives {secondInt = 4, string = "Two fives and a four"}

This simply changes the value of secondInt and string in threeFives and saves it to a new record.

It's often very useful to print a record for debugging or display purposes. We could write a function to turn our records into a String, but that is unnecessarily labor-intensive. Thankfully, Haskell has a way to automate this. We simply have to add deriving (Show) to the end of our data definition.

data MyType = ThreeInts {firstInt :: Int,
                         secondInt :: Int,
                         thirdInt :: Int,
                         string :: String} deriving (Show)

Now, we can use the function show to turn a MyType into a String, and if we type the name of a MyType into GHCi, it will print out a nice string representation of it. For example, typing

twoFives

prints ThreeInts {firstInt = 5, secondInt = 4, thirdInt = 5, string = "Two fives and a four"}.

Now, what if we want MyType to hold three ints and a String or two floats and a String? We can do that!

data MyType = ThreeInts Int Int Int String
            | TwoFloats Float Float String

This may look completely alien to you depending on what languages you're familiar with. In languages like Java or Python, a piece of data can have exactly one structure. In some languages, including Haskell, we have something that computer scientists call algebraic data types, which sounds fancy, but just means that pieces of data of the same type can have different structures.

So a MyType can have either one of those two layouts. If we're writing a function that works on MyTypes, how do you handle the two possibilities? Simple! We use pattern matching.

howManyNumbers :: MyType -> Int
howManyNumbers (ThreeInts _ _ _ _) = 3
howManyNumbers (TwoFloats _ _ _)   = 2

We can also pattern match using a case expression:

howManyNumbers :: MyType -> Int
howManyNumbers record = case record of
    ThreeInts _ _ _ _ -> 3
    TwoFloats _ _ _   -> 2

The ability for a type to have multiple structures is super useful. You may have heard that Haskell doesn't have null pointers. In many languages, null pointers are used to indicate that the function failed or, for some other reason, couldn't return a value of the type you expected. In Haskell, we can't do that (because null pointers are unsafe). Instead, we can do something like this:

data Maybe a = Just a | Nothing

This is frequently called an "optional type". If a function returns Maybe Int, it can either return something like Just 5 if it succeeds, or Nothing if it fails. Because we, the programmer, can see from the function's type that it might return Nothing, it's explicitly clear that we have to deal with this possibility. This allows us to write equally flexible code while avoiding the null-dereferences crashes that sometimes plague other languages.

Parametrized Data Types

Let's make our own version of a list, but for Ints only.

data List = Cons Int List | Empty deriving (Show)

That is, a List can either be a Cons, which has some Int as well as the rest of the List, or it can be empty.

Now, these are both equivalent:

1 : (2 : (3 : []))
Cons 1 (Cons 2 (Cons 3 Empty))

We can write functions on our list type:

len :: List -> Int
len Empty = 0
len (Cons _ tail) = 1 + len tail

But our list still isn't as powerful as the built-in list, because we can only put Ints in it. Well, thankfully there's an easy way to make the type of the list contents variable.

data List a = Cons a (List a) | Empty deriving (Show)

a is called a "type variable". It lets us vary the type of the contents of the list. For example, if we had a List String, it would be Cons String (List String) | Empty.

We can write functions that work on any type of list:

-- We don't care what "a" is
len :: List a -> Int
len Empty = 0
len (Cons _ tail) = 1 + len tail

Or just for certain types of lists:

listConcat :: List String -> String
listConcat Empty = ""
listConcat (Cons str tail) = str ++ listConcat tail

Typeclasses

Remember the thing with Num? Let's explore that a bit more. Look at the type of +.

:type (+)

(+) :: Num a => a -> a -> a. Let's re-examine what this means; + takes 2 things of the same type a and returns another a, but a has to be a Num. That makes sense. You can add Floats to Floats, and Ints to Ints, but you can't add Floats to Ints or []s to []s. (Well, you can concatenate them, but that's not numerical addition.)

How does Haskell know what's a Num and what's not? The answer is that Num is a typeclass. Typeclasses are things that describe "classes" of types. For example, numbers are a class of types. (Don't confuse this with e.g. Java's or Python's classes; in Haskell, "class" means what it does in English.) There's more than one number type, but they all share some common operations. It makes sense to define all those operations in one place, so that we can write functions that use those operations on any type of number.

If you're familiar with Java, a typeclass is kind of like an interface. To be a member of a typeclass, you have to implement whatever functions the typeclass requires. Let's look at how this works with Num. Using Hoogle, we can search for "Num", which brings us here. (Note: Hoogle is a super awesome tool that lets you look Haskell functions up by their type. So if you think "Hmm, what's the name of that function that gives me the first thing in a list?", you can simply search Hoogle for [a] -> a and it will give you some possible solutions.) As you can see, the documentation for Num describes a "minimal complete definition" (all the functions we have to implement) as well as their types.

Three of those are as follows:

(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a

That is to say, if we want to make some type a a member of the Num typeclass, we have to write plus, minus, and times for a.

Let's write out a partial implementation of Num for tuples of Nums. You will want to put this in "Program.hs", as typeclass implementations span multiple lines.

instance (Num a, Num b) => Num (a, b) where
    (a,b) + (c,d) = (a+c, b+d)
    (a,b) - (c,d) = (a-c, b-d)
    (a,b) * (c,d) = (a*c, b*d)

The first line means "If a is a Num type and b is a Num type, then tuples of type (a,b) are also Nums." We then have to back up this claim by providing an implementation for all the required functions.

Try loading "Program.hs" into GHCi. You'll get a warning about how we didn't implement all the required functions for Num, but you can ignore that since we're just experimenting.

Now, if you try running

(1,2) + (3,4)

it will work, because + works on Nums, and tuples of numbers are Nums now!

Let's try making our own typeclass. How about we write one for things that can be mapped over? For example, we can map over a list, but we could also map over something like

data FiveOf a = FiveOf a a a a a

Here's a typeclass for things that can be mapped over:

class Mappable m where
    mapIt :: (a -> b) -> m a -> m b

Notice that mapIt has sort of the same type as map, but instead of [a] -> [b] or List a -> List b or something, it just has m a -> m b. Therefore, anything that's a member of Mappable can be used with this function.

Let's first make a Mappable implementation for List. Note that -- begins a comment.

instance Mappable List where
    mapIt f Empty = Empty -- We don't need to do anything
    mapIt f (Cons head tail) = Cons (f head) (mapIt f tail)

Now let's make one for FiveOf. Remember, the type has to be (a -> b) -> FiveOf a -> FiveOf b.

instance Mappable FiveOf where
    mapIt f (FiveOf a b c d e) = FiveOf (f a) (f b) (f c) (f d) (f e)

Boom! Now we can call mapIt on a List or a FiveOf.

(FYI, there's already a typeclass for this exact thing in Haskell, called Functor.)

IO

There's a lot of mysticism flying around the internet about how Haskell does IO (input/output), like reading files or printing to the console. In reality, it's not all that complicated if you approach it without getting caught up in the theory of it.

Unfortunately, this is one of those things where some explanations work well for some people and not so well for others. Really, once you understand what's going on you'll see that it's very simple. I can't speak for anyone else, but coming from a C/Java/Python/etc. background, Haskell's IO was just very different from anything else I'd used, and it took a bit of getting used to.

Now that we have "Program.hs", we can actually load it into GHCi and play with it. Run (at your terminal, not in GHCi)

ghci Program.hs

Now, everything you've defined in "Program.hs" (like double) is available in GHCi.

Let's check the type of main.

:type main

According to GHCi, main :: IO (). You should read this as "An I/O action that, when run, returns a ()". A () (pronounced "unit") is just a placeholder value that carries no information. Returning () is kind of like returning void in C or Java. It just means that you don't return anything useful.

This is the part that tripped me up when I was learning about Haskell. You may be asking "Isn't a function of type Int -> () also something that, when run, returns a ()?" The problem here is that many languages don't really distinguish between running and evaluating something, while pure functional languages like Haskell force us to do so.

I will do my best to explain the difference between running (an IO action) and evaluating (a function).

When you "run" an IO action, it's allowed to do things that have an effect on the world. For example, it can read a file, or write to the console, or write some data to a pointer. So when we say that an IO Int "returns" an Int, it means that if you were to run that IO action, which might involve reading a file or writing to memory or something, it would end up producing an Int.

When you "evaluate" a function, it's not allowed to do anything that might have a permanent effect on the world. Obviously, under the hood, it has to do some stuff in memory, but these are temporary effects that aren't observable without a debugger. If you have a function of type Float -> Int, the only thing it's allowed to do is look at that Float (or maybe just ignore it) and generate an Int. It's not allowed to read a file, or connect to the internet, or do anything like that.

This is the part that trips some people up; main is just a value, like any other value in a Haskell program. The value of main describes some sort of action that can be run. Us programmers don't know how to actually run these actions; only the compiler knows how to do that. When the compiler makes our program, it looks for the value called main, and sets up the program so that something (specifically, the Haskell runtime) actually does run the action we've named "main".

Let's try making a Hello World. First, let's take a look at putStrLn.

:t putStrLn

putStrLn :: String -> IO (). That is, it takes a string and returns an IO action. Presumably, if we give it the string "Hello World", it will return an IO action that (when run) will print "Hello World".

Let's try it. In "Program.hs", change main to

main = putStrLn "Hello World"

and re-compile.

It works! The program ran the IO action that putStrLn "Hello World" returned.

Let's look at a few other useful IO actions.

:type getLine

getLine :: IO String. This is the first IO action we've seen that returned something other than (). As you may have guessed, IO String means "an IO action that, when run, returns a String". Again, this is in the second sense of "return", as getLine is not a function; it only "returns" a string in the sense that we get a String out if we somehow run the action.

At this point you may be asking "If getLine doesn't actually return a string, and is only an action that theoretically generates a string when run, how do I actually do anything with that string?"

This question actually touches on an interesting property of IO actions. There's no way to run an IO action in a regular function. (Well, it's technically possible, but it's only used for writing very low-level libraries.) The only IO action that gets run is main. However, you can lump multiple IO actions into one IO action, so you can actually run all the IO actions you want by lumping them together and calling it main. The important thing here is that regular functions cannot run IO actions, which means that regular functions don't have "side effects", like reading a file or writing to console.

To actually answer the question, here's how you can do stuff with the string "returned" whenever we run getLine:

main = getLine >>= putStrLn

This looks a little daunting, but it's pretty simple. Check the type of >>=:

:type (>>=)

(>>=) :: Monad m => m a -> (a -> m b) -> m b. OK, so Monad is a typeclass. Just like + works on Nums, >>= works on Monads. I'll talk a bit about Monads later, but just know that IO is a Monad, so we can use >>= on IO actions.

To make things a little simpler to reason about, let's replace m with IO (just like how we can replace (+) :: Num a => a -> a -> a with (+) :: Int -> Int -> Int if we know the specific type of a from context).

(>>=) :: IO a -> (a -> IO b) -> IO b

That's a little better!

Now, take a look...

What if we set a to be String and b to be ()? Then, the type becomes

(>>=) :: IO String -> (String -> IO ()) -> IO ()

Now wait a minute! getLine :: IO String and putStrLn :: String -> IO (). They fit into >>= perfectly!

The return type of getLine >>= putStrLn is then IO (), which is exactly what we need for main.

Perfect!

If it helps, you can think of >>= as creating an action that "runs" the first action, feeds the result into a function that makes a second action, and then runs the second action. This intuition doesn't necessarily hold up for all uses of >>=, but it works fine if you're just using >>= to combine IO actions.

By the way, >>= is pronounced "bind", because it "binds" two actions together.

(If, at this point, you're thinking "Oh God, do I have to use weird functions like >>= just to read from console?", don't worry! The answer is a definitive "No!". I'll build up to how we actually usually do it.)

Combining Lots of Actions

OK, so we can now combine certain types of IO actions. Specifically, if we have action a that returns some value, and a function that takes the value and makes action b, we can combine them.

What if we wanted to read two lines from input but ignore the first input? Let's think about this.

getLine :: IO String
(>>=) :: m a -> (a -> m b) -> m b

So we can't do getLine >>= getLine, because the second getLine doesn't have the correct type. We have to put the second getLine inside a function somehow. Well, we could just do this:

getLine :: IO String
(\_ -> getLine) :: a -> IO String

That's just a lambda function that ignores the argument. It has the correct type! Let's try it:

getLine >>= (\_ -> getLine)

It works! We've combined two getLines, ignoring the result of the first one. It turns out this is a pretty useful trick, so there's a built-in function for it.

(>>) :: Monad m => m a -> m b -> m b
a >> b = a >>= (\_ -> b)

We can also use this to combine multiple IO actions that "don't return anything" (although they actually just return ()). For example, we can do

main = putStr "Hello " >> putStrLn "World!"

OK, what if we want to read a line and then print it back twice? Well, we could just duplicate the string and print it once, but let's say for the sake of edification that we want to run putStrLn twice.

Well, we could do it like this:

main = getLine >>= (\s -> putStrLn s >> putStrLn s)

Man, these things are getting complicated! Surely there's a better way to do this!

Indeed, there is. Fortunately, with a bit of syntactic sugar, we can do all this stuff in a very familiar way. First, notice that getLine >>= \s ... is basically assigning a value to a variable s. In procedural languages, that usually looks something like s = getLine() or s := getLine(). Let's start by sugaring that:

main = do
    s <- getLine
    putStrLn s >> putStrLn s

OK! So we've simply moved some things around (and added a do), but it's pretty obvious how to get back to our original expression that used >>=.

Now, we can sugar the >> like this:

main = do
    s <- getLine
    putStrLn s
    putStrLn s

Cool! Looks just like we're used to in imperative languages. The (important) difference is that we're not actually "running" any actions yet; we're actually just (once this gets de-sugared) using >>= to combine a bunch of IO values that represent runnable IO actions. Nothing gets run until the runtime starts running main. To show what I mean, let's call this something besides main:

myAction :: IO ()
myAction = do
    s <- getLine
    putStrLn s
    putStrLn s

main = myAction >> myAction >> myAction

Even though myAction looks like an imperative program, we can pass it around just like any other value and apply functions to it just like any other value. As you can see, when main gets run, it will run myAction three times. We could also write

main = do
    myAction
    myAction
    myAction

And that's basically everything you need to know to get started!

Monads

I'm going to keep this section brief. I don't think a detailed Monad explanation belongs in a crash course, and I think experience is a much better teacher than explanation in this case.

I said earlier that IO is a Monad. What does this mean?

Simple. Just like Nums support (+) :: Num a => a -> a -> a, (*) :: Num a => a -> a -> a, etc., Monads support (>>=) :: Monad m => m a -> (a -> m b) -> m b and return :: Monad m => a -> m a.

These aren't magical functions or anything. Functional programmers discovered in the 90s that these functions were useful for representing certain things.

I already described how (>>=) is useful for representing the combination of IO actions.

Some other common Monads are Maybe and Either. These monads are usually used to represent functions that can fail.

For example,

failsOnZero :: Int -> Maybe Int
failsOnZero 0 = Nothing
failsOnZero n = Just (n + 1)

failsOnTen :: Int -> Maybe Int
failsOnTen 10 = Nothing
failsOnTen n = Just (n - 1)

doBoth :: Int -> Maybe (Int,Int)
doBoth n = do
    a <- failsOnZero n
    b <- failsOnTen n
    return (a,b)

In this case, if either failsOnZero or failsOnTen fails, the entire computation will fail and return Nothing. The behavior of the Nothing monad is that if any function combined using (>>=) returns Nothing the whole thing returns Nothing.

I don't expect you to understand exactly how this works yet, but try playing around with this. Try reading Real World Haskell's chapter on error handling for some more in-depth examples.