Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2017-07-08.
Ok so we previously looked at reading our first program and began to understand it. Let’s look at some more programs and expressions. This time, though, we’ll focus on the types of the elements to explain what’s going on.
Here is a
"Dolly wants a cracker"
A nice way to think about values is as if they’re magical puzzle pieces that can shrink and grow as needed to fit together. If values are puzzle pieces in this analogy, then their types would be the shapes of the puzzle pieces. This analogy only really works for simple types, but it can be handy.
So, perhaps we might think of that
String value like this:
Let’s look at a definition for this
String value. We make definitions so we can re-use things in other parts of our program later on. We’ll name this one
m (as in message):
m = "Dolly wants a cracker"
As we’ve seen briefly before, this is telling Haskell that we want to define the name
m as being the
String that is literally
"Dolly wants a cracker". Writing definitions for names like this is just like writing a mini dictionary for Haskell, so it can find out what we mean when we use these names in expressions in other parts of our programs.
Now we’ll see what it looks like when we include the code to describe the type of
m :: String m = "Dolly wants a cracker"
Writing the type annotation like this helps both us and Haskell to know what types values and expressions are. This is also called a type signature. You probably worked out that
(::) specifies the type of a name in the same way that
(=) is used to specify the meaning of a name.
Haskell now knows that
"Dolly wants a cracker" whenever we use it in another expression, and that it is a
String. Anywhere we use the
m variable, Haskell will use our
String value; they now mean the same thing.
Now, what about
putStrLn? As we know, this is a name that Haskell already has ready-made for us. Let’s look at its type:
putStrLn :: String -> IO ()
(Note: You will never need to write this type in your own code because it's already defined. We’ve just shown it here so you can see what it is, and how to work with it.)
We can read this as “
putStrLn has type
IO action”, or “
putStrLn is a function from
String value to
String is the function’s input type,
(->) signifies that it's a function, and goes between the input and output types, and
IO () is the function’s output type.
Let’s look at what an
IO () value might look like if we imagined it as a puzzle piece (the shape is completely made up, but we'll use it to explain the types of values):
putStrLn? Well, it’d need be a value of type
IO () once it had a
String popped into it. We could imagine that it looked like this, roughly:
It’s not an
IO () value. It’s something that can be, when supplied with a
String value. See how that makes it a kind of mapping between values of these two types?
When we put a
String value in that pink gap, we get an expression that will evaluate to a value whose type is
putStrLn "Dolly wants a cracker" :: IO ()
As you can see, we can put a type signature on almost any expression. For example, here’s another expression that means the same thing, but with too much annotation:
putStrLn ("Dolly wants a cracker" :: String) :: IO ()
We put a sub-expression type annotation for the
String as well as an annotation for the whole expression! Very silly. In real code, we’d never see an expression like this, because Haskell can almost always work out the types from the context, which is called type inference. Notice we’re using parentheses around the string so Haskell knows which signature goes with which expression.
Of course we could use our
m that we defined earlier instead, and show off its type annotation, too:
putStrLn m :: IO ()
So this expression is an
IO () value, and because
main needs to be an
IO () as well, we can see that one possible definition of
main could be this
putStrLn expression. Here’s the whole program to print out the
m :: String m = "Dolly wants a cracker" main :: IO () main = putStrLn m
So far so good, now let’s see some similar things visualised slightly differently:
When we put a value next to a function, like in the expression
putStrLn "No!", we can say
putStrLn is taking the literal
String whose value is
"No!" as its argument. The word argument means the parameter to a function. In Haskell, wrting two things with a space between usually means that the thing on the left is a function and it will be applied to the value or expression on the right.
A function is a value that expresses a particular relationship between the values of types. That is, it relates one set of values to another set of values. The
putStrLn function relates
String values to particular kinds of
IO actions, for example (ones that will output the input
String values). If you plug (or connect) a
String value into
putStrLn as we have seen above, together they form an expression of type
Almost always when we write Haskell programs, we try to make sure our functions are complete. This means that we’ve written them in such a way that they work with every possible value for their input types.
The “hole” position that
putStrLn has is called an argument (or parameter). By giving a value to a function as an argument as described, we are telling Haskell to connect them into one expression that is able to be evaluated, into a value of its return type.
In the picture above,
main is shown as being square-shaped. By itself,
putStrLn doesn’t have the same shape as
main, so it needs something “plugged” into it before we can equate it to
String is plugged in, the whole thing is an expression with the same shape that is required for
main, which means we can write a program with the definition for
main we saw above.
main :: IO () main = putStrLn "No!"
Another way to say this is that the
IO action that results from connecting these together is a value. Before it has the
String connected to it, it's an
IO () value that is a function of its
String argument (and that function is named
putStrLn). You can see how this means that functions are a mapping from any value of their input type to a particular value of their output type.
It’s important to remember that Haskell receives programs as text files, so there is nothing stopping you from writing programs that make no sense to it, such as trying to provide something other than a
String as an argument to
main :: IO () main = putStrLn 573 -- this will not compile!
Haskell won’t let you compile or run obviously incorrect programs, though. It will give you an error if you try. You should try to compile the incorrect program above. Haskell will tell you there is a type-checking error.
So, we saw that
main needs its definition to be of a certain “shape”. Haskell requires that
main is an
IO action. Its type is written
IO (), which is an
IO action that returns nothing of interest (but does some action when executed). We call this “Nothing of Interest” value Unit, and it’s written like this:
(). That is both its type, and how we write its value. Later you’ll see that this is called an empty tuple, which is just another name for an emptly wrapper that can have nothing in it. We use the double-colon operator
(::) to mark the type that a definition, expression or value is “of”. Below, we’ll see this in operation some more.
Let’s take a look at these two programs which were referenced in the drawings earlier:
main :: IO () main = putStrLn "Yay" main :: IO () main = putStrLn "No!"
By the way, you can only define an identifier once in each Haskell file, so don’t try to put both the above definitions in one file and expect it to compile. Haskell will give you an error. An identifier is just another name for a variable or term, as discussed in an earlier chapter. However Haskell won’t let you confuse it by naming two different things the same identifier, so trying to is an error.
The first program prints out “Yay” on the screen, and the second one prints “No!”
As discussed we read the
(::) operator as “has type”.
IO () is the type of
IO actions that wrap the empty tuple. The empty tuple is a container that can never have anything inside of it, so it’s used as a simple way to express the value of having no value at all.
Handily for us,
IO () is the same type of value that
putStrLn returns when we give it a
String as an argument.
Let’s read another program that does exactly the same thing as the No! program above, but goes about it a little differently:
message :: String message = "No!" main :: IO () main = putStrLn message
We’ve seen this before: we just pulled the No!
String out into its own definition (naming it with the identifier
message), with its own type signature. Don’t let it scare you, it’s pretty simple. It just means
message is a
String, and its value is
Do you have to write type signatures? Not always! We started the book with a definition for
main that had no type signature, and then added one, so you can leave them off — as long as Haskell can infer what you mean by itself. Don't worry, it'll tell you if it can't work it out.
Haskell uses a pretty nifty feature called type inference to figure out what types things should be if you don’t write type signatures for them.
However, it’s often a good idea to put type signatures in so you and others know what your code means later. We recommend doing this because it improves the readability of your program, too.
Did you notice you can use type signatures to let you work out what to plug in to what? They can be incredibly useful when programming.
Your homework is to do an internet search on Haskell programs and see if you can identify at least ten definitions, and ten type signatures. Don’t get too worried by odd looking things, just stick to the homework. We want to get you familiar with what these things look like.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.