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 String
value:
"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
, too.
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 m
means "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 String
to IO
action”, or “putStrLn
is a function from String
value to IO
action”. 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):
What about 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 IO ()
.
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 String
:
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 IO
action.
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 main
.
Once the 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 putStrLn
.
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 "No!"
.
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.