title | author | date |
---|---|---|
Wise Man's Haskell |
Andre Popovitch |
!quickref(This is an unfinished instructional guide for Haskell beginners by André Popovitch.)
!newthought(I'm André Popovitch). This is my book on learning Haskell, an excellent functional programming language. It's not finished yet but hopefully it will be soon - it should be quite useful already. I assume some programming knowledge, but nothing too in depth. If you've played with Python or Javascript and know how to open a command prompt/terminal, that'll be plenty.
What to do if you see a typo: Message me on Twitter, mention me with @likliklik in the Functional Programming Discord, email me, or leave an issue on the GitHub. These are also great ways to ask questions!
!newthought(I wrote) this book because all the other good Haskell tutorials either cost money or were too verbose. That's surprising, considering GHC (the most popular Haskell compiler) has been around since 1992.
This book is heavily inspired by Learn You A Haskell by Miran Lipovača, Haskell Programming From First Principles by Christopher Allen Julie Moronuki, Programming in Haskell by Graham Hutton, Real World Haskell by Bryan O'Sullivan, John Goerzen, and Don Stewart, and Le Petit Prince by Antoine de Saint-Exupéry.
A note on exercises: I've put a few exercises in here, and they're very important for reinforcing your knowledge. Don't skip them!marginnote(This book also has margin notes scattered throughout, I recommend reading them because they often provide extra context or supplemental info.)
A note on quick references: These are intended to be a useful reference when glancing at this book, and as a way of priming your mind about the sort of information you'll be learning in each chapter
I said this book wouldn't be verbose, so let's move on. And have fun!
!quickref(Haskell is a pure functional lazy programming language. Functional languages focus on composing functions together.)
This section is for people with previous experience with statically-typed languages such as C++ or Java. It provides a nonessential high-level overview of some features in Haskell, and can be freely skipped without issue.
The core of Haskell is that you write many small functions, and compose them together into more and more complicated functions. Almost every feature Haskell has is intended to make composing functions more powerful. One way it does this is by having a very useful type system, which lets you compose functions together without fear that you're misusing them. It has easy support for generalization, which allows you to write functions that work with many types. However, you almost never need to actually write the types, because Haskell can use type inference to deduce the types of values in your code.
Haskell functions, by default, are pure. This means they will not do anything except return a value (they will not have side-effects), and their output will only depend on their input. This also helps with composing functions - you can use a function without worrying if it's doing some weird behavior you're not aware of. You're still able to write code with side effects, but it tends to be contained into certain areas.
Haskell does not have null
, but because of its powerful type system you can add the ability for a function to return "no result". But this is stated in the type system, which makes it very easy to see whether a function has a chance of not returning a useful result!marginnote(If this interests you, look up the Maybe
monad.).
Haskell values are typically immutable, which means they will never change. This forces you to simplify your code and makes everything much easier to reason about.
Haskell is lazy by default, which means values are evaluated when they're needed. This allows you to make data structures which you would never see in a non-lazy language, such as lists that are infinite in length, but only the values actually used by the code will ever be evaluated.
All of these features combine to make a language which may be unlike anything you've used before, but allows you to create code which is exceptionally simple and powerful. Haskell has a reputation for being difficult to comprehend or program in, but this is mostly due to the poor quality of teaching materials that some newcomers stumble upon.
!quickref(Haskell provides functions like +
and *
.
Here is how someone would define a constant:
i = 3
Here is how someone would write a function that adds one:
addOne x = x + 1
One compiles Haskell with GHC. GHCi is an interpreted environment useful for playing with Haskell code, accessible by entering ghci
into a terminal after installing The Haskell Platform. To exit GHCi, type :quit
.
)
!newthought(To program) in Haskell, you should be familiar with how to navigate your terminal!marginnote(If you're on Windows, what you call the command prompt is called the "terminal" elsewhere. Also, it's not strictly necessary but you should install Full Cmder, and use that instead of cmd
.). To actually use Haskell, you should download and install The Haskell Platform, which has everything you need to get started quickly. It includes GHC!marginnote(GHC stands for Glasgow Haskell Compiler.), the most popular Haskell compiler, along with useful tools such as Stack and Cabal.
Once you've done that, open a new terminal and type ghci
!marginnote(The i in GHCi stands for interpreter, which is a simple interface for playing with Haskell). If you already had one open, you might need to close and reopen it. Once you've entered that, you should see something like:
GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help
Prelude>
If you need to get out of GHCi, you can type :quit
!marginnote(:q
will work instead of :quit
, if you feel the need to save a couple keystrokes.).
Where you see Prelude>
is you can type Haskell expressions or statements, and see what they evaluate to. If you type an expression, GHCi will tell you the result of that expression. Try entering our friend from the last chapter, 1 + 2
.
Prelude> 1 + 2
3
If you try that and see 3
, all is well. You can also try -
for subtraction!marginnote(-
can also be used to negate a number, such as turning 3
to -3
.), *
for multiplication, /
for division, and ^
for exponentiation, and `rem`
for remainder!marginnote(`rem`
divides two numbers and returns the remainder. It's usually used to check if two numbers are divisible). If you play around with these in GHCi, it might look something like this (To explain what's going on, we've added some comments, which begin with two --
hyphens and are strictly for educational purposes):
-- This is what comments look like.
-- You can type a comment into GHCi, but it won't do anything.
Prelude> (13 * 4) / 3
17.333333333333332
Prelude> 3 ^ 3 ^ 3 ^ 2
-- Omitted, but it's a pretty big number.
-- Let's check if some numbers are divisible by 3 using `rem`,
-- which tells us the remainder of the left argument divided by the right.
Prelude> 10 `rem` 3 == 0
False
Prelude> 12 `rem` 3 == 0
True
You can define constants, just as you might expect.
Prelude> i = 3
Prelude> 4 * i
12
There's few restrictions that to what you can name a constant, but it can't begin with an uppercase letter.
These operators we keep using, like +
and *
, are all examples of things called functions - let's try writing some of our own! In GHCi, type
Prelude> addOne x = x + 1
Prelude> addOne 3
4
Can you see what's going on here? We've written a function called addOne
!marginnote(It's convention in Haskell that if a function name is composed of multiple words, the first letter of each word after the first one is capitalized. That's why we have addOne
, not addone
. Haskell won't let you start a function name with an uppercase letter.), which does exactly what it says on the tin (If you're coming from other languages, notice that Haskell doesn't use parentheses or commas in function calls). When we write addOne 3
it's the same as writing 3 + 1
. You can think of addOne
as a kind of machine - you put a number in, and it spits a different number out. We say addOne
takes a number as a parameter!marginnote(While they're not technically equivalent, we will use the words "parameter" and "argument" interchangeably.), and returns a different number.
!figure(Demonstration of how addOne
works.)(/static/wise_mans_haskell/assets/exp_addOneDemo.svg)
Exercises:
-
Write a function that adds two to a number,
addTwo
. -
Write a function that adds multiplies a number by two, called
timesTwo
.
!newthought(You can) have a function with more than one parameter, too!marginnote(As we'll discuss later, functions with more than one variable are somewhat of an illusion in Haskell.). Let's see how that works.
Prelude> addThenDouble x y = (x + y) * 2
Prelude> addThenDouble 2 4
12
This is a function that takes two numbers, adds them together, then doubles the result. Writing addThenDouble 2 4
is the same as writing (2 + 4) * 2
or just 12
.
You're able to use your custom functions when writing new ones, and it's where the real power of functions appears:
Prelude> addOneTwice x = addOne (addOne x)
Prelude> addOneTwice 4
6
The parentheses are used to tell Haskell we want to do addOne x
first, then run addOne
on the result of that. Omitting these parentheses will result in an error. So we've seen that we can have functions with one or two variables, but how about... zero variables?
Prelude> always3 = 3
Prelude> always3
3
If you like, you can think of this as a function that takes 0 values and returns 3
!sidenote(The proper name for a function that takes no values would be a nullary function.). However, this is better thought of as just a plain value or constant. GHCi will allow you to define i = 1
and then later define i = 2
, but this is not allowed in "real" Haskell (which we'll go over in a moment).
!newthought(Now, this) is all well and good, but not very conducive to writing actual programs. For that, you need to be able to write your code into a text file and run it. To do that, make a new file somewhere on your computer called myProgram.hs
!marginnote(.hs
is the extension used for most Haskell programs.). Inside it, write:
-- myProgram.hs
!include(haskelltests/should_compile/myProgram.hs)
The comments!sidenote(starting with --
, remember) at the top of these examples are what we named the file in our own tests. Now, in your terminal, navigate to the directory where the file is located!marginnote(remember, you can get out of GHCi by typing :quit
). Then open up GHCi again and type:
Prelude> :load myProgram.hs
*Main> addOne 100
:load
is a special GHCi command!marginnote(Anything that starts with a :
is a GHCi command, not "real" Haskell.) - it tells GHCi to interpret the file you give it and load it into your interpreter so you can play with it. You defined addOne
in myProgram.hs
, so by typing :load myProgram.hs
you load all the functions in that file.
You might have also noticed Prelude>
changed to *Main>
, and you'll see why that happens in the future. But for now, you can have fun playing with it - try running :set prompt "Haskell is fun> "
.
Exercises:
-
What do you think that the following function will do?
foo x = x * 3
If you're unsure, you can try entering it into GHCi and playing with it.
-
In GHCi, write a function called
circumference
, which takes a number and multiplies it by3.14
. -
In GHCi, write a function called
doubleDifference
that takes two values, subtracts the second from the first, then doubles the result. Stuck? Try looking at some of the functions we made earlier in this chapter.
Now would also be a good time to turn on multiline mode in GHCi with :set +m
. This allows you more to easily write expressions in GHCi that span multiple lines.
Prelude> :set +m
Prelude> 2 + 4 + (
Prelude| 9 / 3)
9.0
!quickref(Types are labels attached to all the values in a Haskell program. Haskell functions take specific types, and you won't be able to compile any code that tries to pass a function a value of the wrong type.
Some types are Bool
and [Char]
.
A type is written in the form of a type signature, which looks like value :: type
. A type signature can be viewed by entering :t <value>
into GHCi.
Types can be grouped into typeclasses, which allow a single function to work with multiple types.)
!newthought(You've already) seen you can set a constant to a number:
Prelude> i = 1
Prelude> i
1
But there's lots of other types of things you can set constants on, too! Go back to GHCi and try this:
Prelude> c1 = 'a'
Prelude> c2 = 'b'
Prelude> c1
'a'
Prelude> c2
'b'
What you're seeing here is we can set a constant to a Char
(a single character), instead of a number. Do String
s work?
Prelude> s1 = "abc"
Prelude> s1
"abc"
They do! In Haskell, you use single quotes for Char
s and double quotes for String
s.
Booleans are also available in Haskell, like so:
Prelude> a = True
Prelude> a
True
Prelude> b = False
Prelude> b
False
There are (almost) infinite different numbers you can choose from, but Booleans only have two allowed values: True
and False
!sidenote(There's also undefined
, which is a bit of an exception to every rule and not something you should worry about.). Booleans are called Bool
s in Haskell.
!newthought(You can) test the equality of two values using ==
. To see if they're not equal, you use /=
!marginnote(The ==
and /=
functions take two values of the same type and return a Bool
.).
Prelude> a = 1
Prelude> b = 1
Prelude> c = 3
Prelude> a == b
True
Prelude> a == c
False
Prelude> a /= c
True
That works on numbers, String
s and Char
s, too:
Prelude> a = 'a'
Prelude> b = 'b'
Prelude> a == b
False
Prelude> a /= b
True
There are some functions that take Bool
s, of course. There's &&
, which means "and", and ||
, which means "or". These functions each take two Bool
s and return a new Bool
.
-- You can use && to mean "and" and || to mean "or", too.
Prelude> 1 == 1 && 2 == 2
True
Prelude> 1 == 1 && 2 == 3
False
Prelude> 1 == 1 || 2 == 2
True
Prelude> 1 == 1 || 2 == 3
True
Prelude> 1 == 3 || 2 == 3
False
!newthought(Haskell keeps) track of what type everything is!marginnote(You can think of Haskell types as little labels that are attached to everything in your program.). If i
is a number and c
is a Char
, it doesn't make sense to ask if one is equal to another - they're two different types of things, of course they aren't equal! If you try to run "5" == 5
, Haskell will make an error, because even though they look similar, one is a String and one is a number, and both arguments to ==
need to be the same type. Division only works on numbers, so Haskell won't let you write 5 / 'a'
. The goal of types is to make it harder to accidentally pass the wrong value to a function, and it's immensely useful. This is called a type system, and lots of languages have one, but Haskell's is better than most other languages'.
!newthought(Lists are) also possible in Haskell, let's check those out.
Prelude> myNumberList = [1, 2, 3]
Prelude> myNumberList
[1,2,3]
Prelude> myCharList = ['a', 'b', 'c']
You might have noticed that if you type myCharList
to tell GCHi to show it to you, then you see:
Prelude> myCharList
"abc"
What gives? It's saying "abc"
, but we typed ['a', 'b', 'c']
! Well that's because in Haskell, a String
is just a List of Char
s. So when you type "abc"
, Haskell treats it as of you typed ['a', 'b', 'c']
. And when you ask GHCi to display a List of Char
s, it shows it as "abc"
instead of ['a', 'b', 'c']
because "abc"
is easier to read and write.
!newthought(Now, you're) going to see a very important GHCi command. It's called :type
, or :t
for short. Every value in Haskell has a type, and :t
tells you what that type is. Let's try on some of our constants:
Prelude> a = 'a'
Prelude> :type a
a :: Char
This is GHCi telling us our 'a' is of type Char
. The ::
is just a bit of syntax GHCi uses to tell you what it's talking about, it doesn't affect the type.
Prelude> s1 = ['a', 'b', 'c']
Prelude> s2 = "abc"
Prelude> s1 == s2
True
You can have lists of other things too, by the way.
Prelude> i1 = [1,2,3]
Prelude> i1
[1,2,3]
Prelude> i2 = ["hello", "world"]
Prelude> i2
["hello","world"]
i1
is a List of numbers. i2
is a List of String
s. And since a String
is really just a List of Char
s, i2
is a List of Lists of Char
s!marginnote(One rule about Haskell Lists: everything in them has to be of the same type. If you try i = [1, 2, "a"]
, GHCi will give you an error.)!
Lists have some useful features. You can use <>
to concatenate two lists!marginnote(You may also see ++
used to concatenate two lists. This works but is older and less preferred. If you've used other programming languages, you can think of <>
to be "overloaded" to work on many different types while ++
is limited to only working on lists.).
Prelude> s1 = "Hello"
Prelude> s2 = "World"
Prelude> s1 <> s2
"HelloWorld"
Whoops! We forgot to put a space in there.
Prelude> s1 = "Hello"
Prelude> s2 = "World"
Prelude> s1 <> " " <> s2
"Hello World"
Let's write a function to do this for us, so we don't have to it every time.
Prelude> concatWithSpace l1 l2 = l1 <> " " <> l2
Prelude> concatWithSpace "Hello" "World"
"Hello World"
See, it's pretty easy, huh? If you'd like to be able to put concatWithSpace
between the two String
s, like you can with <>
, simply surround it with `
backticks.
Prelude> "Hello" `concatWithSpace` "World"
"Hello World"
If all you need to do is put a value at the beginning of a List, you can do so with the :
operator!marginnote(You might want to remember this, because :
turns out to be a very fundamental operation for Lists!).
Prelude> 1:[2,3]
[1,2,3]
!newthought(As you) just learned, when you have a constant, it can be a number (like i = 2
), a Char
(like c = 'x'
), or a String
(like s = "hello"
), a List of Strings, a List of Numbers, or one of many other types.
!marginfigure(Some examples of types and type signatures.)(/static/wise_mans_haskell/assets/exp_typesAndSignatures.svg)
In Haskell, every value has a type. We can figure out a value's type with the :type
command in GHCi, which shows us it's type signature. A type signature is just a way of writing down a value's type. You can also use :t
instead of :type
. Let's try it out.
Prelude> i = 'a'
Prelude> :type i
i :: Char
GHCi is trying to tell you that i
has the type Char
. To do that, it says i :: Char
. You can read ::
as "has the type". Let's try another.
Prelude> s = "hello!"
Prelude> :type s
s :: [Char]
This is GHCi telling you s
has the type [Char]
, which means it's a List of Chars
(A String
is the same as [Char]
). The square []
brackets are telling us it's a List of Chars, not just a regular Char
.). Do you remember what type True
is?
Prelude> :type True
True :: Bool
Ah yes, a Bool
. But even functions have types! Let's write a function to test this, addABang
, which takes a String
and puts an exclamation mark at the end.
Prelude> addABang s = s <> "!"
Prelude> addABang "hello"
"hello!"
Works like a charm. Now let's see what type it has.
Prelude> :type addABang
addABang :: [Char] -> [Char]
The ->
arrow means we're dealing with a function. In our case, a function that takes a [Char]
and returns a [Char]
.
Remember the concatWithSpace
function? We can use :type
to see what type it has too.
Prelude> concatWithSpace l1 l2 = l1 <> " " <> l2
Prelude> :type concatWithSpace
concatWithSpace :: [Char] -> [Char] -> [Char]
You might be wondering what multiple ->
arrows mean. That's how functions with multiple parameters are typed in Haskell. The thing after the last arrow is the output, all the rest are the parameters. So concatWithSpace
takes two [Char]
s and returns a [Char]
!sidenote(You might be wondering why it's done like this. Isn't it confusing that the return type looks just like a parameter? Well, it has to be this way, because of a Haskell feature called currying (which will be explained in a later chapter).).
concatWithSpace :: [Char] -> [Char] -> [Char]
-- ================ ======
-- input output
When writing Haskell functions, it's usually a good idea to write the type signatures explicitly, even though you usually don't need to. Let's make a new file, testingTypes.hs
. Inside it, put:
-- testingTypes.hs
!include(haskelltests/should_compile/testingTypes.hs)
What we did here is write a type signature for our function. This just means we're telling Haskell, and also anyone who reads this code, that concatWithSpace
should have the type [Char] -> [Char] -> [Char]
(meaning it takes a [Char]
, then another [Char]
, then returns a [Char]
). This is important so let's go over that once again. Here is our type signature:
concatWithSpace :: [Char] -> [Char] -> [Char]
-- =============== == ==============================
--- [1] [2] [5]
-
concatWithSpace
: This tells Haskell that this type signature is for theconcatWithSpace
function. -
::
: This separates the name of the function with the type we're saying it has. You can read this as "is a". -
[Char] -> [Char] -> [Char]
: This is the type of the function. This function takes a[Char]
, then another[Char]
, then returns a[Char]
.
<>
, ==
, +
, -
, /
, *
, and ^
are all functions. Because they're written only with special characters, they're also called operators. Operators are by default infix functions, which mean they go in between their parameters, e.g. 1 + 2
.
You can turn an infix function into something that works more like a regular function by surrounding it in parentheses. So instead of writing 1 + 2
, you can also write (+) 1 2
. Do you remember the ++
operator, which could be used instead of <>
? Lets examine its type. :type
wants a regular function, not an infix function, so you have to surround ++
in parentheses.
Prelude> :type (++)
(++) :: [a] -> [a] -> [a]
Strange! Instead of writing [Char]
or any other actual type, they've written [a]
. Notice the lowercase letter - all actual types start with uppercase letters. This means a
is just a placeholder, or a type variable, for any type you want to give the function. When you pass <>
a [Char]
, Haskell sees that [Char]
can fit into [a]
(since a
can be any List). But it also requires consistency! Once Haskell knows that a
is Char
for this function call, then you can't later pass it an [Int]
or [String]
or anything besides [Char]
. This will give you an error:
Prelude> [1, 2, 3] ++ [True, True, False]
-- [Int] [Bool]
-- Error!
This makes sense, when you remember that all the values in a list need to be the same type. Here's another example. The head
function takes a List and gives you the first element of it.
Prelude> head [1, 2, 3]
1
What do you think its type will be?
Prelude> :type head
head :: [a] -> a
So this is telling us head
takes a List of some type, and returns an element of that type. This is what we would expect! It'd be very strange if we gave it a List of Bool
s and it said the first element was a Char
.
Exercises:
-
What type will
head
give us if we give it a[Char]
? -
There's a function called
tail
which is similar tohead
. It takes a List and returns a List of everything except the first element. Sotail "abc" == "bc"
andtail [1,2,3] == [2,3]
. Can you guess what type signature tail has? To check your answer, run:type tail
in GHCi. -
You might be wondering why we looked at the type of
(++)
and not(<>)
. Try examining the type of(<>)
and notice how it uses something we haven't seen yet - we'll discuss it in the next chapter.
!quickref(Typeclasses are collections of types, intended to allow a single function to work on multiple types. They're most useful when some types are similar in some way. One example is the Num
typeclass, which contains types such as Integer
and Float
. These types are said to be an "instance" of the typeclass Num
.
Common typeclasses are Eq
(for types where it makes sense to compare them for equality) and Num
(for types which represent numbers).
Typeclasses can be "superclasses" of other typeclasses. For example, the Num
typeclass is a superclass of Eq
, which means everything in the Num
typeclass is required to also be in the Eq
typeclass.
The :info
GHCi command can be used to see what types are in a particular typeclass, or what typeclasses a particular type belongs to.)
!newthought(In most) programming languages, but especially Haskell, there are some types that aren't exactly the same but have some things in common. For instance, take the following Haskell types:
-
Integer
- Exactly what it sounds like. Integers are numbers like-1
,0
,1
, or1000
, but not2.5
. It has no maximum or minimum value, but if you try to express too large of a number, eventually your computer will run out of memory. -
Int
- This is the same "int" you might be familiar with from other languages. It has a maximum and minimum value, which can vary depending on what machine you're on, but it is usually very large (generally the maximum is 2^31-1 and the minimum is -2^31). The tradeoff is that it's more efficient thanInteger
. -
Float
- Short for floating point. This holds numbers that can have decimal points, like1.4
or-2.5
, but can still hold whole numbers, like2
. The drawback is that sometimes the numbers it holds aren't exactly the number you think it is. This causes the scary floating point precision error, where answers end up being just a little bit away from what they should be. Enter0.1 + 0.2
into GHCi to see how easy it is to trip up on this. -
Double
- This is the same as aFloat
, but it's twice as precise. You can still run into floating point precision errors though.
All four of these seem kind of similar. If you write a function that multiplies a number by two, it'd be nice you didn't have to say specifically what type it takes, and could instead just say you want it to take in a number. Well luckily, that is very easy!sidenote(Uniquely easy, in fact. In languages like C++ you'd have to do all kinds of fiddling with templates to get a result that still isn't as nice as Haskell's.)! Let's write a function that doubles Int
s.
intDoubler :: Int -> Int
intDoubler x = x * 2
!newthought(All of) the types mentioned above are inside a typeclass!marginnote(Typeclasses have nothing to do with classes from most other languages! Don't get confused.) called Num
. That means you can use +
, -
, *
, and abs
on them (abs
returns the absolute value of a number). Now, we'll show you how to modify intDoubler :: Int -> Int
with some new syntax to allow it to work on anything in the Num
typeclass, not just Int
:
numberDoubler :: (Num a) => a -> a
numberDoubler x = x * 2
Let's take a closer look at that type signature
numberDoubler :: Num a => a -> a
-- ============ == ======== ========
-- [1] [2] [3] [4]
-
numberDoubler
- The name of our function. -
::
- Remember, you can read this as "is a". -
Num a =>
- Puts a restriction on the type variablea
. Justa
on its own can be anything, but when you putNum a =>
at the beginning of a type signature, whatever typea
takes the place of is constrained to be part of theNum
typeclass. This is called a type constraint, because it constrains what types are allowed (a
would normally allow any type, but because of the type constrainta
now has to be a number). You can think of=>
as using whatever is on the left side to constrain the right side. -
a -> a
- The part of the type signature that should be familiar to you. It means we're dealing with a function that takes ana
and returns ana
. Thanks toNum a =>
, we have the constraint thata
be inside the typeclassNum
, but otherwise, we aren't picky.
The result is a function that can double anything in the Num
typeclass, which is all our number types!sidenote(If this seems intimidating, read on, more examples will make it clear.)!
!newthought(You'll see) here that a type can be a part of more than one typeclass. These are some typeclasses you'll be seeing a lot of:
-
Eq
- This is the typeclass for anything that can be tested for equality, using the==
and/=
functions. Most types, includingChar
s,String
s, and everything in theNum
typeclass, fall underEq
!sidenote(In fact, almost all types except functions are inside theEq
typeclass.).Prelude> 2 == 2 True Prelude> :type (==) (==) :: Eq a => a -> a -> Bool
You can see
==
is a function that takes two values of typea
, wherea
is inside theEq
typeclass, then returns aBool
. -
Ord
- This is the typeclass for things that can be put in an order. They have functions like>
,>=
, etc. Anything that is part of theOrd
typeclass is automatically part of theEq
typeclass!sidenote(Ord
is separate fromNum
because, for example, letters can be put in alphabetical order. Also, some numbers can't be ordered, like complex numbers.).Prelude> 3 > 5 False Prelude> :type (>) (>) :: Ord a => a -> a -> Bool
>
takes two values of typea
, wherea
is of the typeclassOrd
, and returns aBool
. -
Show
- This is the typeclass for things that can be converted to strings with the functionshow
. That's actually what GHCi does whenever you enter an expression - if it's of typeclassShow
, then it callsshow
and prints the result!sidenote(If it isn't in the typeclassShow
, you'll see an error in GHCi.).Prelude> show 2 "2" Prelude> :type show show :: Show a => a -> String
show
takes a value of typea
, wherea
is of the typeclassShow
, and returns a String. -
Read
- This is the typeclass for things that can be converted from strings with the functionread
. It's like the inverse of the Show typeclass. It's a bit tricky because if it doesn't know what type to return it'll give you an error. Luckily we can tell it with::
!sidenote(Which should be familiar to you, as it's the same::
you see in type signatures!):Prelude> read "5" -- error! read doesn't know if we want an Int, an Integer, etc. Prelude> read "5" :: Int 5
Let's look at the type of
read
, as well:Prelude> :type read read :: Read a => String -> a
So
read
takes a string and returns a value of typea
, wherea
is part of theRead
typeclass. -
Integral
andFloating
. -Integral
contains the typesInt
andInteger
, andFloating
contains the typesFloat
andDouble
. You'll see them often in one useful function,fromIntegral
, which can convert anIntegral
into whateverNum
is necessary:-- Remember, we can force a number to be an Int with, for example, `4 :: Int`. Prelude> i = (4 :: Int) Prelude> i 4 Prelude> f = (3.5 :: Float) Prelude> f 3.5 -- You can't add values of two different types! Prelude> i + f -- error! -- But we can convert i, which is an Int, to any other number using fromIntegral: Prelude> (fromIntegral i) + f 7.5
The
fromIntegral
function is useful when a function returns anInt
orInteger
and you need it to be some other number type. Lets see what type it has:Prelude> :type fromIntegral fromIntegral :: (Integral a, Num b) => a -> b
That's right, this one involves two different typeclasses. If you have more than one typeclass in your function's type signature, they should have
()
parentheses around them and be separated by commas. You can describe this function as taking an Integral and returning a Num. -
Fractional
- this is the typeclass for numbers that don't have to be whole numbers.Fractional
contains bothFloat
andDouble
.
Exercises:
-
What do you think the type signature of
+
is? Type:type (+)
to check your answer. -
What do you think the type signature of
/
is? Type:type (/)
to check your answer.
You can also use the :info
command to get a bunch of information about a typeclass.
Prelude> :info Fractional
class Num a => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
{-# MINIMAL fromRational, (recip | (/)) #-}
-- Defined in `GHC.Real'
instance Fractional Float -- Defined in `GHC.Float'
instance Fractional Double -- Defined in `GHC.Float'
This is telling us that any type in the the Fractional
typeclass provides us with the functions /
, recip
, and fromRational
. It also tells us that Float
and Double
are both instances of Fractional
. the :info
command is very useful if you're learning about a new typeclass and want to explore what you can do with it.
:info
can also be used to tell you information about a type, including all the typeclasses that type is an instance of.
Prelude> :info Int
data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in `GHC.Types'
instance Eq Int -- Defined in `GHC.Classes'
instance Ord Int -- Defined in `GHC.Classes'
instance Show Int -- Defined in `GHC.Show'
instance Read Int -- Defined in `GHC.Read'
instance Enum Int -- Defined in `GHC.Enum'
instance Num Int -- Defined in `GHC.Num'
instance Real Int -- Defined in `GHC.Real'
instance Bounded Int -- Defined in `GHC.Enum'
instance Integral Int -- Defined in `GHC.Real'
!quickref(Lists are a way to store multiple values of the same type. A list of Bool
s would be written as [True, True, False]
and have the type [Bool]
.
Tuples are a way to store a set number of values of different types. A tuple of one Bool
and one Char
would be written as (True, 'h')
and have the type (Bool, Char)
. A tuple with 2 items is known as an 2-tuple, 3 items is a 3-tuple, etc.
There are many useful functions on lists, such as head
which retrieves the first value of a list.
There are some useful functions for tuples, like fst
which retrieves the first value of any 2-tuple.)
!newthought(Lists)!marginnote(Haskell Lists are actually represented internally as singly-linked lists, which has all the performance implications you'd expect. Haskell also has Arrays which you can use where appropriate. We'll discuss this more in the chapter on writing optimized Haskell.) are very useful in Haskell, and there are a lot of functions that work on lists that you should know about.
-
x:xs
inserts a valuex
at the beginning of a Listxs
.Prelude> 3:[4,5] [3,4,5] Prelude> 1:[] [1] Prelude> 1:2:[4,5] [1,2,4,5]
This is very important because it's actually how lists are implemented internally. When you type
[10, 20, 30]
, Haskell actually sees that as10:20:30:[]
. -
take x xs
gives you the firstx
values from the Listxs
.Prelude> take 5 [1,2,3,4,5,6] [1,2,3,4,5]
-
drop x xs
drops the firstx
values from the Listxs
.Prelude> drop 3 [1,2,3,4,5,6] [4,5,6]
-
head xs
gives you the first value of the Listxs
.Prelude> head [1,2,3,4,5,6] 1
-
last xs
gives you the last value of the Listxs
Prelude> last [1,2,3,4,5,6] 6
-
init xs
gives you all but the last value of the Listxs
.Prelude> init [1,2,3,4,5,6] [1,2,3,4,5]
-
tail xs
gives you all but the first value of the Listxs
.Prelude> tail [1,2,3,4,5,6] [2,3,4,5,6]
-
xs !! x
returns thex
th item of the Listxs
. You may also see`elem`
which is equivalent.Prelude> xs = "Ice, ice, baby" Prelude> xs !! 0 'I' Prelude> xs !! 11 'a'
-
length xs
returns the length of the Listxs
.Prelude> length "Ice, ice, baby" 14
-
null xs
returns whether the List is empty. It is better to use this thanxs == []
, but it does the same thing.Prelude> null "Ice, ice, baby" False Prelude> null [] True
-
sum xs
takes a List of numbersxs
and returns their sum.Prelude> sum [1,2,5] 8
-
product xs
takes a List of numbersxs
and returns their product.Prelude> product [1,2,5] 10
-
maximum xs
andminimum xs
do what you would expect, they take a List of values in the typeclassOrd
and find the maximum or minimum one, respectively.
!newthought(Haskell provides) some easy ways to get a range of numbers, or other things that can be enumerated, using the ..
syntax.
-- Get the numbers between 1 and 10 inclusive.
Prelude> [1..10]
[1,2,3,4,5,6,7,8,9,10]
-- Get the letters between 'a' and 'r' inclusive.
Prelude> ['a'..'r']
"abcdefghijklmnopqr"
-- Get the numbers between 1 and 10, with a step of 2.
Prelude> [1,3..10]
[1,3,5,7,9]
-- The step is mandatory if you want to go downwards.
Prelude> [10,9..1]
[10,9,8,7,6,5,4,3,2,1]
You don't have to provide an upper limit to your List, either!
-- This makes a List containing [1,2,3,4,5,...] all the way up to infinity.
Prelude> [1..]
-- If you type this in GHCi, it will keep showing numbers until you hit ctrl+c, which is how you tell a Haskell command to stop running.
-- This makes a List containing [0,10,20,30,...] all the way up to infinity.
Prelude> [0,10..]
-- Again, have your control-c key ready.
-- To do anything useful with infinite lists, you usually need to limit which ones you get.
Prelude> take 10 [10,20..]
[10,20,30,40,50,60,70,80,90,100]
!newthought(Infinite lists) work because Haskell is lazy - it won't compute anything until it absolutely has to!sidenote(which is typically when it needs to be displayed on the screen, written to a file, or something similar.). If you do something that forces Haskell to evaluate the whole list, it will never finish, as the list is infinite. Be sure to do something like take 10
on the list before you try and print it, so you don't get stuck doing stuff forever.
!newthought(If you've) used List comprehensions in Python, these should be pretty familiar. They're ways to turn one List into a new List - by filtering it and transforming the values.
Prelude> xs = [1..5]
Prelude> xs
[1,2,3,4,5]
-- Here is a List comprehension that takes all the values in xs and does nothing with them.
Prelude> [x | x <- xs]
[1,2,3,4,5]
-- Here is a List comprehension that doubles all the values in xs.
Prelude> [x * 2 | x <- xs]
[2,4,6,8,10]
-- Here is a List comprehension that squares all values in xs, and keeps the result if the value is higher than 10.
Prelude> [x ^ 2 | x <- xs, (x ^ 2) > 10]
[16,25]
Let me break down the structure of a List comprehension:
[x ^ 2 | x <- xs, (x ^ 2) > 10]
-- ===== ======= ===========
-- [1] [2] [3]
-
x ^ 2
: This is the value that will appear in the final List.x
is a variable that comes#2
. This part is used to transform the contents of the List. -
x <- xs
: This can be translated as "iterate over the values in the Listxs
and assign them tox
". It's used to decide which List you want to draw from. -
x * x > 10
: This is a condition!marginnote(You can also have multiple conditions here, separated by commas.) that determines whether the result of#1
will be included in the List. This is used to filter the contents of a List.
In Python, this would be written as [x*x for x in xs if (x * x) > 10]
.
!newthought(let's say) you wanted to have a List of points, where a point is an x
and a y
coordinate. You could have a List of Lists, like this:
Prelude> points = [[(0 :: Double), 1], [-2, 4], [5, 2]]
Prelude> :type points
points :: [[Double]]
But there's an issue here. If you accidentally forgot a coordinate in your point, that's certainly an error, and Haskell wouldn't warn you!marginnote(An important philosophy in Haskell is that it should be as hard as possible for you to make an error that the compiler doesn't warn you about. We do that by structuring our program so incorrect states (such as points with only one coordinate) are forbidden by the type system. The more you do this, the more bug-free your code will be.)!
So a better solution is to use tuples. You make a tuple the same way you make a List, but you use ()
parentheses instead of []
square brackets.
Prelude> points = [((0 :: Double), (1 :: Double)), (-2, 4), (5, 2)]
Prelude> :type points
points :: [(Double, Double)]
You'll notice the type signature changed! Tuples are different from lists. They don't have to be homogenous (you can have multiple different types in the same tuple), so their type is the type of each of their elements. A tuple containing two doubles will have the type (Double, Double)
.!marginnote(The order is important. If the tuple's type is (String, Bool)
you can't assign (True, "Hello")
to it.)
!newthought(A tuple) with 2 items is called a 2-tuple, a tuple with 3 items is called a 3-tuple, etc. 2-tuples and 3-tuples are also called doubles and triples, respectively.!marginnote(You can have a tuple with 1 or 0 items, but there wouldn't be much point.)
!newthought(If you) have a 2-tuple, you can extract values from it using the fst
and snd
functions!marginnote(Their type signatures are fst :: (a, b) -> a
and snd :: (a, b) -> b
respectively.). For example, fst (1, 2) == 1
and snd (1, 2) == 2
. Tuples are useful when you want to have a function that returns multiple values. For example, you could imagine a person's name being represented as a 2-tuple, where the first element is their given name and the second item is their family name. Here's a function that will tell you the full name of a child, given their first name and their parent's full name!sidenote(Of course, don't use this in production, as not everyone has a name that fits this schema.).
Prelude> myName = ("Jane", "Doe")
Prelude> child'sName parentsName child'sFirstName = (child'sFirstName, snd parentsName)
Prelude> child'sName myName "Claire"
("Claire","Doe")
!newthought(Another useful) function is zip
. It's very simple, so you might even be able to guess what it does just from it's function signature!
zip :: [a] -> [b] -> [(a, b)]
What it does is take two lists, and zip them up into a list of 2-tuples.
Prelude> zip [1,2,3] "abc"
[(1,'a'),(2,'b'),(3,'c')]
If one list is longer than the other, it cuts off at the shorter list. This is useful because it means you can easily use infinite lists with it!
Prelude> zip [1..] "abc"
[(1,'a'),(2,'b'),(3,'c')]
Exercises:
-
Above, you say:
points = [((0 :: Double), (1 :: Double)), (-2, 4), (5, 2)]
And the type signature of
points
waspoints :: [(Double, Double)]
. What would it have been if we just wrotepoints = [(0, 1), (-2, 4), (5, 2)]
instead? Check your answer with:type points
.
!quickref(For convenience in writing functions, Haskell provides many shortcuts such as pattern-matching and guards.)
!quickref(Here is an example of pattern-matching:
f (pattern₁) = x₁
f (pattern₂) = x₂
Pattern matching can also be used to pull values out of lists.
head (x:_) = x
Patterns are checked top-to-bottom. If none of the patterns match, the program crashes.)
!newthought(You'll notice) we haven't learned how to do if/then/else in Haskell. There is a way, but when possible, it's preferred that you use pattern matching!marginnote(Pattern matching (and case
, discussed below), were created to make it easier to write programs that work well with the type system. If/then/else is useful occasionally, but really struggles with certain Haskell features such as Maybe
, which we'll discuss later. Also, pattern matching looks nicer.). Let's steal an example from the excellent Learn You A Haskell:
-- luckyNumber7.hs
!include(haskelltests/should_compile/luckyNumber7.hs)
You'll notice that, if you type a number other than 7, we don't really care what it is. We just tell you that you're unlucky. If you don't care about a certain value, it's a Haskell tradition to assign it to the variable _
!sidenote(Specifically, GHC won't warn you about unused variables if the unused variable begins with a _
).
-- luckyNumber7u.hs
!include(haskelltests/should_compile/luckyNumber7u.hs)
This makes implementing recursive functions (functions which call themselves) very easy. Factorial is the usual example:
-- factorial.hs
!include(haskelltests/should_compile/factorial.hs)
Can you see what's going on here?
-
If you call
factorial 0
, it just returns1
. -
If you call
factorial 1
, it returns1 * factorial (1 - 1)
which evaluates to1 * factorial 0
which evaluates to1 * 1
which evaluates to 1. -
If you call
factorial 2
, it returns2 * factorial (2 - 1)
which evaluates to2 * factorial 1
. We knowfactorial 1
evaluates to1
, so2 * factorial 1
evaluates to2 * 1
which evaluates to3
.
If you have a pattern that would match any possible value, it is said your pattern is exhaustive. Otherwise, it's non-exhaustive. Here is an exhaustive pattern:
-- isTrue.hs
!include(haskelltests/should_compile/isTrue.hs)
Bool
can only be True
or False
, so all the bases are covered. However, this would be non-exhaustive:
-- writtenNum.hs
!include(haskelltests/should_compile/writtenNum.hs)
If you pass 4
to this function, your program will crash!marginnote(Later in this book I will discuss a setting in GHC you can turn on that will force all your patterns to be exhaustive.). A common trick to make patterns exhaustive is to put a catch-all at the bottom:
writtenNum :: Integral a => a -> String
writtenNum 1 = "One"
writtenNum 2 = "Two"
writtenNum 3 = "Three"
writtenNum _ = "unknown :("
But beware that you don't accidentally put the catch-all at the top! Patterns are checked top-to-bottom, so if you had a catch-all at the top, none of the other patterns could ever be hit.
You can use pattern matching in more interesting ways as well, because you can actually match the inside of certain values, such as tuples. Remember the fst
and snd
functions from the last chapter? They only apply to 2-tuples. Let's try making some that work with 3-tuples.
-- tuplePatterns.hs
!include(haskelltests/should_compile/tuplePatterns.hs)
It's kind of a pain having to write these by hand. A later chapter will describe how to write them for all tuples, automatically.
The same idea works with lists. Let's make a function that's like head
, but gives you the second value instead of the first.
-- neck.hs
!include(haskelltests/should_compile/neck.hs)
If you're wondering what these weird :
colons are doing, remember from the chapter on lists. The List you type as [3]
, Haskell sees as 3:[]
!marginnote([3]
gets converted to 3:[]
before any real work is done.). The String
(really, [Char]
) you type as "abc"
, Haskell sees as 'a':'b':'c':[]
. Try entering 'a':'b':'c':[]
into GHCi to prove it to yourself.
So a:b:c
can match 1:2:[]
with a = 1
, b = 2
, c = []
. It can also match 1:2:3:4:5:[]
with a = 1
, b = 2
, c = [3,4,5]
!marginnote(Note that, when pattern matching lists with :
, the first items are single elements and the last item is a (sometimes empty) List.). It will actually match any List with at least two items! This means this pattern is non-exhaustive, since it won't work for any lists with less than two elements, but that's okay for our purposes.
Lets use this newfound knowledge to write a length'
!sidenote(The '
apostrophe is an allowed character in Haskell variable and function names, typically pronounced "prime".When making a copy of a function, it's common to put a '
at the end of it to differentiate it.) function that uses recursion and pattern matching:
length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs
If these remind you of case
statements in other languages, they should. Pattern matching in Haskell is really just syntax sugar!sidenote(Syntax sugar is an addition to a language that is designed to make things easier to read or to express.) for case
statements.
They tend to look like this:
case expression of pattern -> result
pattern -> result
pattern -> result
The length'
function from earlier could be written with a case statement, like this:
length'' :: (Num b) => [a] -> b
length'' xs = case xs of [] -> 0
(_:xs) -> 1 + length' xs
Exercises:
-
What do you think will happen if we called
factorial
with a negative number? Hint: try running it, but don't hold your breath for an answer -
What do you think will happen if you call our
neck [1]
? What do you think will happen if you callhead []
? -
Write a function that gets the first value from a 4-tuple.
!quickref(Here is an example if an if/then/else expression:
if <condition>
then <expression>
else <expression>
These can be used anywhere Haskell expects an expression and can be nested.)
!newthought(In Haskell), if and else are expressions. They're analogous to the ternary operator from other languages. If you have an if
, you have to have an else
. Here's an example taken from the Haskell Wikibook:
-- describeLetterIfElse.hs
!include(haskelltests/should_compile/describeLetterIfElse.hs)
The general structure of an if/then/else expression is:
if [condition] then [value if condition is True] else [value if condition is False]
Be careful to have proper indentation - it can lead to compile errors if you don't!sidenote(The general rule is, if you make a newline when writing an expression, the following code should be indented further than the beginning of that expression. See the Haskell Wikibook for more details.).
!quickref(Guards are another way for a function to return different values for different inputs. Here's an example of guards being used to write a function that takes the absolute value of a number
abs' n
| n < 0 = -n
| otherwise = n
Guards are matched top-to-bottom, and otherwise
matches anything.)
!newthought(Guards are) like if/then/else, only more readable if you have a lot of branches. Here's the above describeLetter
function implemented with guards (notice that you don't use a =
sign to define a function if you're using guards).
describeLetter :: Char -> String
describeLetter c
| c >= 'a' && c <= 'z' = "Lower case"
| c >= 'A' && c <= 'Z' = "Upper case"
| otherwise = "Not an ASCII letter"
otherwise
is a value that is equal to True
, being used as a catch-all here. True
could be used instead, but otherwise
is more readable. Just like with patterns, guards are evaluated top to bottom, so don't put the catch-all at the top. And be careful to line up your guards correctly, because it looks better but also because your code won't compile if you don't.
!quickref(let
expressions and where
statements are ways to give a name to a value inside a function.
Here is the structure of a let
expression:
let name₁ = value₁
name₂ = value₂
... in <expression>
Here is an example of a where
statement:
f x = y
where y = ... x ...
Whether to use let
or where
is a matter up to the programmer's taste. let
can be used in any expression, but where
can only be used with functions.)
!newthought(When writing) a function, sometimes it's nice to be able to bind names to intermediate values. There are two ways to do that in Haskell: let
Expressions and where
statements.
-
let
expressions!sidenote(These are true expressions - you can use them anywhere Haskell expects an expression.) - these take the form oflet [variable bindings] in [expression]
. Here's an example:-- computeEnergy.hs
!indent(!include(haskelltests/should_compile/computeEnergy.hs)) ```
You can have multiple bindings in one `let` expression, and use bindings in the definition of other bindings.
```haskell
-- makeURL.hs
!indent(!include(haskelltests/should_compile/makeURL.hs)) ```
As you may have noticed, the order of the bindings doesn't matter. Since these are expressions, you can use them anywhere, not just in functions.
-
where
statements - these aren't expressions, they're statements, and can only be used in functions!sidenote(And a few other places, but functions are where you'll see them most often). They're useful because they work perfectly with guards, where let expressions don't really.-- benchPressJudger.hs
!indent(!include(haskelltests/should_compile/benchPressJudger.hs)) ```
You might notice something looks strange about our type signature - what's that `(Ord a, Fractional a)`? It means whatever type `a` is has to be an intance of `Ord` (so that we can see if it's larger or smaller than stuff), and `Fractional` (so we can use it with fractional numbers when converting pounds into kilograms). Not all fractional numbers have to be orderable, that's why we needed to have the `Ord` constraint as well!sidenote(To avoid this in your own code, there are some specialized typeclasses such as `RealFloat`, but `(Ord a, Fractional a)` is perfectly readable even if it feels somewhat silly.).
Also, pattern matching works almost anywhere you see a `=` sign. So this would have worked, too, if we were very interested in terseness:
```haskell
where benchPressKgs = benchPressLbs * 0.4535
(weak, decent, strong) = (67, 125, 250)
```
!newthought(You need) some practice reading compile errors because you'll be doing it a lot. Luckily, the time you lose in fixing a battery of compile errors, you gain by removing many opportunities for runtime errors! Not all of these examples are fair because some are issues we haven't told you about. That's why you have to actually run them, to get good practice! The answers are at the bottom of the section
-
Run this in GHCi:
Prelude> area x = .5 * 3.14 * x^2
You should see something like:
<interactive>:78:10: error: parse error on input `.'
Where I have
78
, you'll have a different number. That tells you the line number the error was on, but in GHCi, it tells you the number of lines you've entered before now. We can ignore that since it isn't super useful. The number after the colon (in our case,10
) is the column the error was on. This is useful because that should be fairly close to the source of the mistake. 10 letters into our command is the start of.5
. Can you figure out the error? -
Make a new file named
errorTest1.hs
. Inside it, write:-- errorTest1.hs
!indent(!include(haskelltests/error_examples/errorTest1.hs)) ```
Then try to load it into GHCi with `:load errorTest1.hs`. It should give you an error like the following:
```haskell
errorTest1.hs:5:6: error:
parse error on input `='
Perhaps you need a 'let' in a 'do' block?
e.g. 'let x = 5' instead of 'x = 5'
|
5 | bar = a - a
|
```
Can you spot the error? The error message isn't very helpful here.
-
Now that you got that figured out, lets do one last one. Make a file called
errorTest2.hs
. Inside it, put:-- errorTest2.hs
!indent(!include(haskelltests/error_examples/errorTest2.hs)) ```
Then go to GHCi and run `:load errorTest2.hs`. You should see a bunch of errors that say
```haskell
errorTest2.hs:2:7: error: Variable not in scope: a :: a -> a
```
Can you find the issue?
-
Just one more, this is probably one of the most common errors you'll get. Try writing this:
-- errorTest3.hs
!indent(!include(haskelltests/error_examples/errorTest3.hs)) ```
You should see an error like:
```haskell
..\error_examples\errorTest3.hs:2:21: error:
* Could not deduce (Ord a) arising from a use of `>'
from the context: Num a
bound by the type signature for:
isGreaterThan :: forall a. Num a => a -> a -> Bool
at ..\error_examples\errorTest3.hs:1:1-42
Possible fix:
add (Ord a) to the context of
the type signature for:
isGreaterThan :: forall a. Num a => a -> a -> Bool
* In the expression: x > y
In an equation for `isGreaterThan': isGreaterThan x y = x > y
|
2 | isGreaterThan x y = x > y
| ^^^^^
```
Hint: Make sure to read the error message carefully!marginnote(Which is good advice generally, but especially here.).
And remember, a good first step is always to just Google the error!
Answer 1: You can't write a floating point number like .5
, it needs to be 0.5
.
Answer 2: All declarations must start in the same column in Haskell. Having some of them being offset will make Haskell think you're trying to do something strange. Remove the space in front of the two bar
s.
Answer 3: We have foo = a + a
, but we need to have foo a = a + a
. Otherwise, Haskell has no idea where a
is coming from. Same issue for bar
.
Answer 4: Our only type constraint on our function is Num a
, but we use >
, which is given to us by Ord
, not Num
!marginnote(This commons source of confusion stems from the fact that not all numbers can be ordered, like complex numbers.).
!newthought(None of) these problems are particularly difficult, but they are very mathsy, which may not be your cup of tea. But this chapter will hone your Haskell skills - try to solve the problems yourself! They're well within your abilities by now.
-
Add up all the natural numbers between 1 and 1000!marginnote(Natural numbers are just numbers used for counting, such as 1, 2, 3, etc.), inclusive.
-
Hint! Reread the chapter on lists if you're stuck, especially the chapter on enumerations.
-
The answer found by your program should be 500500
-
-
Add up all the natural numbers that are divisible by 3 or 5, between 1 and 1000
-
List comprehensions will come in useful here.
-
The answer found by your program should be
234168
-
-
Write a function that takes a List of
Num
s, and tells you the difference between the sum of the squares and the square of the sums!sidenote(If you're wondering how to find the sum, Haskell includes asum
function for that exact purpose. Use List comprehensions to square all the numbers before you sum them, too.).- For
[1,2,3,4,5]
, my program got-170
. If yours gets170
then you just put the terms in the opposite order, which is fine.
- For
!quickref(Functions are values, and functions can take other functions as parameters. Functions can be given only some of their arguments (curried), the result of which is another function. For example, (4+)
returns a function which adds 4
.
The map
function takes a function and a list, and applies the function to every value in the list.
The filter
function takes a function which returns a Bool
and a list, and removes the values in the list where the function returns False
.
The (.)
function takes two functions, and returns a new function which applies them in reverse order. So (1+) . (2*)
returns a function which multiplies by two, then adds one.)
!newthought(Functions in) Haskell are values like any other,!marginnote(This is an important part of being a functional programming language.) and can be used in any place you'd use a value normally, including being passed to and returned from functions. Observe:
Prelude> adder a = a + 1
Prelude> adder 3
4
Prelude> otherAdder = adder
Prelude> otherAdder 3
4
See? We've assigned the value of adder
to otherAdder
. We should take a look at the types here.
Prelude> :type adder
adder :: Num a => a -> a
Prelude> :type otherAdder
otherAdder :: Num a => a -> a
Yep, exactly the same. otherAdder
is now a function that's exactly the same as adder
.
That brings us to of one of the most famously complicated things in Haskell: Currying!marginnote(Haskell was actually named after the mathematician Haskell Curry.). The general idea is that functions in Haskell always take 1 argument. When you write a function that seems to take multiple arguments, Haskell converts it to a function that takes one argument, then returns a new function which takes the next argument, etc. That probably seems horribly complicated, So let's see an example. Do you recall the take
function?
Prelude> :type take
take :: Int -> [a] -> [a]
Prelude> take 3 "abcdefg"
"abc"
take
is a function that takes two arguments. But if you only supply it with one argument, what you get is a partially applied function!sidenote(That's what they're called in other languages, anyway.), which needs one more argument to complete.
Prelude> :t take 3
take 3 :: [a] -> [a]
Essentially, take 3
returns a function that takes a list and returns the first 3 values of that list. We can use this function wherever we would write take 3
. Here's an example:
Prelude> myTake3Function = take 3
Prelude> myTake3Function "abcdefg"
"abc"
Prelude> :type myTake3Function
myTake3Function :: [a] -> [a]
Prelude> :type (take 3)
take 3 :: [a] -> [a] -- Spaces for clarity.
This also explains why function type signatures look odd. Remember the type signature for take
.
take :: Int -> [a] -> [a]
Well, that would be more clearly expressed like this:
take :: Int -> ([a] -> [a])
take
is a function that takes an Int
, and returns a function that takes a [a]
and then extracts the first few values from it. When you write take 3
, you're getting a new function that performs take 3
. It's really that simple!
Prelude> :type (take 3)
take 3 :: [a] -> [a]
You don't have to think about this at all when writing functions, this is something Haskell gives you for free. This may seem confusing now, but will become more clear when you see more examples. Remember concatWithSpace
from a few chapters ago?
Prelude> concatWithSpace x1 x2 = x1 <> " " <> x2
Prelude> concatWithSpace "Hello," "World!"
"Hello, World!"
Let's use currying to make a function that says "Hello" to anything.
-- Use currying to make a function that takes a string and puts "Hello " in front of it.
Prelude> sayHello = concatWithSpace "Hello"
Prelude> sayHello "puppy!"
"Hello puppy!"
Prelude> sayHello "house!"
"Hello house!"
-- Now, let's check out our types.
Prelude> :type concatWithSpace
concatWithSpace :: [Char] -> ([Char] -> [Char]) -- Parentheses for clarity
Prelude> :type sayHello
sayHello :: [Char] -> [Char] -- Spaces also added for clarity.
!newthought(Currying also) works with operators, but often requires surrounding them in parentheses!sidenote(Which also turns them from infix functions to regular functions.).
Prelude> addOne = (1+)
Prelude> addOne 5
6
-- You can curry either side of an infix function.
Prelude> addOne' = (+1)
Prelude> addOne' 5
6
Let's rewrite concatWithSpace
to use currying.
Prelude> concatWithSpace' x1 = ((x1 <> " ") <>)
Prelude> concatWithSpace' "hello" "curry"
"hello curry"
!newthought(There's an) incredibly useful function in Haskell named map
. It does something that is quite common in Haskell, but it could be surprising. It has two parameters. The second is a List, but the first one is... another function!sidenote(If you're familiar with functions as parameters from C++, you'll find in Haskell they're much more convenient and type-safe.)! Let's take a look at the map
's type.
Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Let's break this down.
map :: (a -> b) -> [a] -> [b]
-- ======== === ===
-- [1] [2] [3]
-
A function that takes an
a
and returns ab
!sidenote(In the future, I might call this "a function froma
tob
".). -
A List of
a
s. -
A List of
b
s.
Let's put it into practice! What it does will become clear.
-- Take a look at the type of the function (+1).
Prelude> :t (+1)
(+1) :: Num a => a -> a
-- So it's a function that takes a value of type a and returns a value of type a, where a is a Num.
Prelude> map (+1) [1,8,4,1,-4]
[2,9,5,2,-3]
-- By the way when something is expecting an (a -> b) function,
-- an (a -> a) function works fine.
-- a and b can be different, but they don't have to be.
What's happened here is map
has iterated over every element of [1,8,4,1,-4]
, and applied the function (+1)
to it. If you'll look,1
became 2
, 8
became 9
, etc.
Prelude> map (+1) [1,8,4,1,-4]
-- [1, 8, 4, 1, -4]
-- (+1) (+1) (+1) (+1) (+1)
-- [2, 9, 5, 2, -3]
However, there's another function called fmap
, and it's preferred that you use fmap
over map
. fmap
works in situations map
doesn't, so it's a bit nicer!sidenote(Specifically map
works only on Lists, but fmap
works on anything in the typeclass Functor
(which includes Lists). Unfortunately Functor
s were developed after map
so we're stuck with the f
in front.).
!newthought(Lets look) at another function, filter
.
Prelude> :t filter
filter :: (a -> Bool) -> [a] -> [a]
-- [1] [2] [3]
-- 1) function from a to bool
-- 2) List of a
-- 3) List of b
Prelude> isEven x = x `rem` 2 == 0
Prelude> isEven 1
False
Prelude> isEven 2
True
Prelude> :t isEven
isEven :: Integral a => a -> Bool
-- Looks like isEven is a function from a to bool...
Prelude> filter isEven [1..10]
[2,4,6,8,10]
See what happened? filter
"filtered" out the values for which isEven
returned False
!
Often with filter
you need a function fast, but don't feel like giving it a name. For that, it's a good time to use lambdas!sidenote(Also known as anonymous functions.), which look like this:
\i -> i < 4
-- [1] [2] [3]
-
\i
- A backslash!marginnote(Backslash was chosen because looks kind of like the Greek letterλ
lambda, which you may recognize from Haskell's logo.), followed by all the parameters used by your function. In our case we only have one,i
. -
->
- An arrow used to separate the two halves of the lambda -
i < 4
- The body for your lambda - in our case, it returns aTrue
if the number given is less than4
, andFalse
otherwise.!marginnote(If you want, you can give Haskell some type information about your lambdas, too.Prelude> bangBetween = (\x y -> x <> "!" <> y) :: String -> String -> String Prelude> bangBetween "Bang" "Bong" "Bang!Bong"
Here's how we'd use it with filter
:
Prelude> filter (\i -> i < 4) [1..10]
[1,2,3]
Note that, because of the magic of currying, we could have written that as simply:
Prelude> filter (<4) [1..10]
[1,2,3]
filter
is useful for reducing the contents of a list to only contain what you want, and it's used quite a bit!sidenote(As you may have noticed, everything you can do with map
and filter
you can also do with list comprehensions. In fact, list comprehensions are converted to map
and filter
during compilation!).
!newthought(Function composition) is mainly used to make code more readable, instead of being a huge mess of parentheses. It very common, so it's symbol is just the .
period.!sidenote(A period also looks a bit like the function composition operator in mathematics, which is the real reason it was chosen.)
The .
operator is mainly useful for chaining together functions. This is its type:
Prelude> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
-- [1] [2] [3] [4]
-
First, it takes a function from
b
toc
. -
Then, it takes a function from
a
tob
. -
Then, it takes a value of type
a
. -
Lastly, it returns a value of type
c
.
Let's write .
ourselves. It's actually extremely simple.
Prelude> (.) f g x = f (g x)
Here f
and g
are functions. You can use them just like any other function like we do with(g x)
.
So let's see it in action. Here's an example of how we used to do things. We'll make a function that multiplies numbers by 2 and a function that adds 1.
Prelude> times2 = (*2)
Prelude> plus1 = (+1)
Prelude> times2plus1 x = plus1 (times2 x)
Prelude> times2plus1 4
9
Now, let's rewrite times2plus1
to use .
instead!sidenote(The .
operator might not seem very useful now, but it's extremely common so it'll pay to learn how it works.).
Prelude> times2plus1' = plus1 . times2 -- Use currying for conciseness.
Prelude> times2plus1' 4
9
Function composition takes two functions as arguments. Then it returns a new function, which just applies the right function!marginnote(The interesting thing about this is that about half of people think it's backward - the function on the right is applied first, then the function on the left, but some people's intuition says it should be the other way around. Remember, it goes ⬅️this way⬅️) to whatever input it gets, then applies the left function.
You can chain it up as much as you want:
Prelude> plusABunchThenTimes2 = times2 . plus1 . plus1 . plus1 . plus1 . plus1 . plus1 . plus1
Prelude> plusABunchThenTimes2 5
24
Note that this will not work:
Prelude> times2 . plus1 3
-- error!
The issue is that plus1 3
is getting evaluated first, and turning into 4
. Then it sees times2 . 4
, and that doesn't make any sense, since 4 isn't a function. This is what you have to do instead:
Prelude> (times2 . plus1) 3
8
If all those parentheses seem like they could get out of hand, read on!
!newthought(Function application) is something you normally get when you don't do anything!sidenote(You could make a joke about function application being lazy.). It's what happens when you do head [1,2,3]
- the function head
is applied to [1,2,3]
. But sometimes it happens before you want it to. That's where $
comes in. It's a function, and here is its type:
Prelude> :t ($)
($) :: (a -> b) -> a -> b
It takes a function, and an argument, and applies the argument to the function. We could define it like this!sidenote(To make it complete, we'd also have to set the precedence with the infixl
keyword.).
Prelude> f $ x = f x
And here's how we'd use it:
Prelude> head [1,2,3]
1
Prelude> head $ [1,2,3]
1
You may have noticed it doesn't seem to have changed anything. Well, that's because $
doesn't really change anything. But it has one very useful property: it has very low precedence. This means it will happen after most other stuff has gotten done. Remember this issue in the last section?
Prelude> times2 . plus1 3
-- error!
plus1
was getting applied to 3
too soon. We wanted to wait until times2 . plus1
happened, then apply the result!marginnote(As a reminder, the result of times2 . plus1
is a function which adds one, then multiplies by two.) of that to 3
. We can make this dream a reality with $
.
Prelude> times2 . plus1 $ 3
8
Success! Let's try a more complicated usage:
Prelude> sum . take 3 $ [1..1000]
6
This may seem confusing, but so let's add some parentheses to try and make it more clear:
Prelude> sum . (take 3) $ [1..1000]
6
take 3
returns a function that takes 3. It then gets composed with sum
, which sums all the values of a List, to make a new function that applies take 3
then applies sum
. This function is then applied to [1..1000]
, which results in 6
!sidenote(Note that due to Haskell's laziness, this list is never actually fully evaluated or put into memory, it is only ever evaluated up to the 3rd item.). Let's try really going crazy with it.
Prelude> sum . (drop 2) . fmap (+1) . (<>[1,2,3]) $ [-1,0]
9
As you can see, we use .
between all our functions, then $
to the value we want to apply it to. This takes the list [-1,0]
, adds [1,2,3]
to it, adds 1
to every number in the list, removes the first two items from the list, and then gets the sum of that list!sidenote(You typically would want to use let
or where
to make an intermediate list instead of putting them all into one giant function composition chain, just to make it more readable.).
!newthought(There's another) function you should be familiar with: foldl1
. It's useful when you want to reduce a whole list to a single value. Let's use foldl1
to implement sum'
, a function that takes a list of numbers and sums them all up. We'll need to make a new function, add
, that takes two numbers and adds them.
-- Write a function that takes two numbers and adds them.
Prelude> add x acc = x + acc
Prelude> add 1 3
4
-- Use that function to write sum'.
Prelude> sum' xs = foldl1 add xs
Prelude> sum' [1,3,5,9]
18
You're probably wondering how this works. First, notice that add
takes two values, but only returns one? For example, add 1 4
evaluates to 5
. What foldl1
does is use that to take a whole list of values and reduce it down to just one. It starts out by using the function to turn the first two values into one value.
!figure()(/static/wise_mans_haskell/assets/exp_foldl1_step_1.svg)
Notice that the list got shorter! foldl1
just keeps doing that until there's only one item left.
!figure()(/static/wise_mans_haskell/assets/exp_foldl1_repeat_step_1.svg)
There's another function, foldr1
, that does the same thing except it goes right-to-left instead of left-to-right. If possible, you should use foldr1
instead of foldl1
because foldr1
is much more efficient.
Exercises
-
Write a function called
product'
which multiplies the items in the list, instead of adding. -
Write the
foldl1
function yourself, calling itfoldl1'
. It should have the type signaturefoldl1' :: (a -> a -> a) -> [a] -> a
. The answer is in thewritefoldl1.hs
file.-
Hint! You need recursion.
-
Hint! The base case is a list with one value. If you get
foldl1' f [x]
, you can just returnx
. -
Hint! Check out the chapter on Recursion Practice if you're stuck.
-
You may have noticed our add
function, which adds two numbers, is the same as (+)
(due to currying). So this would also work:
Prelude> sum' xs = foldl1 (+) xs
And, of course, due to currying we can do this:
Prelude> sum' = foldl1 (+)
Now, foldl1
is certainly useful, but it's a bit restrictive. If we have a list of numbers, it has to return a number. If we have a list of characters, it has to return a character, and so on. However, there's the foldl
which is very similar but allows the output to be a different type than the input. Because of this, you also need to pass it a separate starting value. Let's use foldl
to write a function that takes a list of numbers and returns a string of all the numbers together. So [1,2,3]
would be "123"
. Remember, the show
function takes a value and returns a String
, so show 3
returns "3"
. We'll also use a lambda to make a function in our expression, instead of defining to separately.
Prelude> listString xs = foldl (\acc x -> acc <> (show x)) "" xs
Prelude> listString [1,2,3]
"123"
Here's how this works:
!figure()(/static/wise_mans_haskell/assets/exp_foldl_repeat_step_1.svg)
Of course, there's also foldr
, which is much faster than foldl
.
Exercises
-
Rewrite
sum'
usingfoldl
instead offoldl1
. Hint: you can add anything to 0 and get the same number. -
Write the
foldl1
function yourself. The base case is the empty list. Look at yourfoldl1'
function for inspiration.
!quickref(undefined
is a value which is a member of every type, meaning you can use it anywhere in place of any value, but any attempt to evaluate it will crash your program.)
!newthought(Haskell allows) you to use a special value called undefined
. This is sometimes also refereed to as bottom
, ⊥
, or _|_
!sidenote(_|_
is just an ASCII version of ⊥
. Both are pronounced "bottom").
Normally Haskell will complain if you use a value of a type it doesn't expect.
Prelude> head "hello!"
'h'
Prelude> head True
-- error! head wants an [a], not a bool.
But there's a special value in Haskell which is acceptable to all functions, undefined
!sidenote(Technically, undefined
is a member of all types.). There's a catch though - if a Haskell program tries to "look at" this value or actually do anything with it, your program will crash.
Prelude> i = undefined
Prelude> i + 1
-- error!
Prelude> l = [1,2,3,4,undefined]
Prelude> l !! 3
4
Prelude> l !! 4
-- error!
This may seem totally useless. What use is there for a value that you can't use? Well, it comes in handy in a couple of situations, like demonstrating laziness. We haven't talked much about laziness, but it's basically the idea that Haskell won't try and compute something it doesn't have to. This is why you can have an infinite List in Haskell - the whole List isn't ever made and loaded into memory, of course. It's just only computed as far along as your program needs. You can demonstrate that the head
function (which takes a List and returns the first value) doesn't look at any values in the List besides the first one.
Prelude> head [1, undefined]
1
Prelude> head [undefined, 1]
-- error!
If a function makes your program crash or gets caught in an infinite loop, we say its return value is ⊥
. Imagine we wrote this:
Prelude> stupid = sum [1..]
Prelude> stupid
-- infinite loop
Trying to evaluate stupid
will cause the program to hang. You can try to sum an infinite List, but you won't ever finish, you'll just keep adding values forever. So the value of stupid
is said to be ⊥
!sidenote(This symbol is used because it looks like it's pointing at the bottom of something. We say "bottom" because of some complicated type theory, but it basically means it's "the most undefined" value.). Once your program hits it, it won't do anything else.
Sometimes you want to talk about the fact that you can pass a ⊥
to a function, and it won't crash your program. Here's an example:
Prelude> weird x = 3
This is a function that takes a value and returns 3
, no matter what you put in. Since Haskell is lazy, it won't try to evaluate x
until you need it, which this function never will since it throws it away and just returns 3
.
So we could write this, and it'll work just fine:
Prelude> weird . sum $ [1..]
3
Haskell never needs the value of sum [1..]
, so it never computes it. But sometimes you want to be able to say this in a way that's a bit more general. That's where undefined
!marginnote(undefined
is a good name because, if a function takes your program into an infinite loop, there's no definition for what that functions should return. It'll never return anything!) comes in:
Prelude> weird undefined
3
Haskell is lazy and so our weird
function doesn't even look at the value of its parameter x
(since it doesn't need it). That's good in our case since the value of x
is undefined
and looking at it would crash our program.
!newthought(Undefined can) also be a very useful tool. Let's say you're writing a program and you're not really sure how to write a function you need. You can just define it as undefined
and then you can go work on something else. Your program won't work until you define that function for real, of course, but you can call it elsewhere and Haskell will do its work in making sure your types check out. Newer versions of GHC make this even easier, with what's called a type holes. Anywhere GHC expects a value you can add a _
and GHC will still allow you to compile but gives you a warning that your function won't work at runtime.
!quickref(Sometimes it doesn't make sense for a function to return a value in all cases. For this reason we have Maybe
. For example, instead of returning an Int
, a function might return a Maybe Int
(this should be noted in it's type signature). Maybe
values are created with the Just
function, so a Maybe Int
could be created with Just 3
. A value called Nothing
is also a valid Maybe <anything>
, so if you make your function return a Maybe Int
, you can return Nothing
if you have no value to return.)
!newthought(Often we) want to write a function that just doesn't make sense for all possible inputs. For example, the head
function!marginnote(head
extracts the first element from a list.). It works great for most lists, but for empty lists, it crashes your program.
Prelude> head [1,2,3]
1
Prelude> head []
-- error!
In other languages, what you might do is return some value like null
or None
. But those come with issues. For example, what if you forget to check to make sure you got a valid answer before using the value? In fact, this causes so many problems that it's sometimes called The Billion Dollar Mistake. Haskell provides a better way, using Maybe
. Here's how it works.
-- maybeHead.hs
!include(haskelltests/should_compile/maybeHead.hs)
We have three new things here. Maybe a
, Just
and Nothing
!marginnote(In case you've forgotten, (x:xs)
is a pattern that will match any list with at least one element, and set x
to the first element.). Maybe a
says that we don't want to return just a regular a
, we want to return it wrapped up in a Maybe
. Maybe a
is a different type than a
, and you can't return an a
if your code expects a Maybe a
. To convert a regular value into a Maybe
value, we use the Just
function!marginnote(Just
takes a value and returns that value wrapped up in a Maybe
type.). But there's another valid Maybe a
value, and that's Nothing
. You can think of Just
as saying "Give me a value of type a
, and I will give you a value of type Maybe a
". Let's play with that a little bit.
*Main> maybeHead ([1,2,3] :: [Int])
Just 1
*Main> maybeHead []
Nothing
See? No more crash. But we have an issue: our output isn't an Int
anymore, it's a Maybe Int
!
*Main> first = maybeHead ([1,2,3] :: [Int])
*Main> :t first
first :: Maybe Int
This makes it a bit less convenient. Maybe Int
is a different type than Int
, and we can't add, subtract, multiply, or do anything else to it. In order to work around thi s we can use pattern matching. We'll write a function that takes a Maybe Int
and increments the number inside by 1
. If you pass it a Nothing
, it'll return Nothing
. We'll also include our maybeHead
function from earlier just to have them in the same file.
-- maybeAddOne.hs
!include(haskelltests/should_compile/maybeAddOne.hs)
And here's how it works:
*Main> first = maybeHead ([1,2,3] :: [Int])
*Main> first
Just 1
*Main> maybeAddOne first
Just 2
We can also use case
expressions to avoid writing a whole function. As an example, let's quickly multiply first
by 10
:
*Main> first = maybeHead ([1,2,3] :: [Int])
*Main> case first of (Just x) -> Just (x*10)
Just 10
You'll notice that we don't have a case for if first
is Nothing
. This means our pattern is non-exhaustive and would crash if we provided it a Nothing
.
*Main> first = maybeHead ([] :: [Int])
*Main> case first of (Just x) -> Just (x*10)
--- error!
!newthought(Type constraints) are allowed when using Maybe
. We wrote Maybe Int
, but we could have used the following instead:
maybeAddOne :: (Num a) => Maybe a -> Maybe a
Which is better because it's more general, since it will add 1
to any number instead of specifically Int
s. We have to have the Num a
constraint because we intend on doing arithmetic with the number.
Exercises:
-
Write a function with the type signature:
maybeSquare :: (Num a) => Maybe a -> Maybe a
It should take a
(Just <number>)
and return aJust (<number>^2)
. If passedNothing
, it should returnNothing
.-
So
maybeSquare (Just 4)
should evaluate toJust 16
-
maybeSquare Nothing
should evaluate toNothing
.
-
-
Write a function with the type signature
maybePow :: Maybe Int -> Int -> Maybe Int
, that takes aMaybe Int
and anInt
, and raises the former to the latter power.-
So
maybePow (Just 3) 2
should evaluate toJust 9
. -
maybePow Nothing 2
should evaluate toNothing
.
-
-
Remember our
neck
function from earlier?neck :: [a] -> a neck (_:x:_) = x
Rewrite it to return a
Maybe a
instead of ana
, and not returnNothing
when provided a list with less than two elements.
!quickref(To create our own data types, we use a data declaration, in the form of:
data MyTypeName = DataConstructor₁ | DataConstructor₂ | DataConstructor₃
Then, to create a value of type MyTypeName
, simply use one of the data constructors, for example by writing DataConstructor₃
. Data constructors can take values, such as
data MyTypeName = DataConstructor Int
Here, to create a value of type MyTypeName
, you might write DataConstructor 3
.
One can also have type variables in their own types. Here is how one might define the Maybe
type.
data Maybe a = Nothing | Just a
Here, Maybe
is a type constructor (and takes a type, such as Maybe Int
), and Nothing
and Just
are data constructors (and take values, like Just 3
).)
!newthought(You know) by now that Haskell offers a variety of data types, like Int
, Integer
, Bool
, and Char
. But like most languages, it also allows you to create your own. Remember how Bool
has two possible states, True
and False
? Let's make one a new type Answer
, that has two states Yes
and No
.
Prelude> data Answer = Yes | No
-- [1] [2] [3] [4] [5]
Let's break this down.
-
data
: This is how we say we're going to declare a new data type!marginnote(In this case, it's what's called a sum type, which is similar to an enum in other languages, only better.). -
Answer
: This is the name of our data type. -
Yes
: This is a possible value our data type can take, likeTrue
orFalse
for the data typeBool
. -
|
: This is telling Haskell we're going to have multiple "constructors" for this data type, which is a fancy way of saying there are multiple ways we can make a value of typeAnswer
. In our case, those two ways are typingYes
orNo
. This makes it a sum type - you can think of it as the sum of multiple different possible values. -
No
: This is the other value our data type can take.
!newthought(An important) restriction to note here is that data types have to begin with capital letters. Person
is a valid data type, person
is not!marginnote(You might have noticed something quite nice here - a type cannot begin with a lowercase letter, and a function or binding cannot begin with an uppercase letter. This makes it simple to tell the difference between Foo
and foo
.).
On Facebook, when you are invited to an event, you can respond "attending", "not attending", or "might attend"!sidenote(Don't let all these enum-ey examples discourage you, we'll get to more complicated uses in a moment.). Let's see how that would be represented in Haskell.
Prelude> data InvitationResponse = Attending | NotAttending | MightAttend
If you've tried to play with them. you might have noticed that you don't seem to be able to use these values for very much.
Prelude> Attending == Attending
-- error!
<interactive>:4:1: error:
* No instance for (Eq InvitationResponse)
arising from a use of `=='
* In the expression: Attending == Attending
In an equation for `it': it = Attending == Attending
Prelude> i = Attending
Prelude> i
-- error!
<interactive>:6:1: error:
* No instance for (Show InvitationResponse)
arising from a use of `print'
* In a stmt of an interactive GHCi command: print it
Everything seems to be causing errors! That's because there are no functions that we can use on our values with. Let's write some. Make a new file called invitationResponseFunctions.hs
.
-- invitationResponseFunctions.hs
!include(haskelltests/should_compile/invitationResponseFunctions.hs)
Now let's load this into GHCi and play with it. To save time, we'll use :l
instead of :load
Prelude> :l invitationResponseFunctions.hs
[1 of 1] Compiling Main ( invitationResponseFunctions.hs, interpreted )
Ok, one module loaded.
*Main> bob = "Bob Vance"
*Main> bobsResponse = NotAttending
*Main> bobsResponse `invitationResponseIsEqual` Attending
False
*Main> bobsResponse `invitationResponseIsEqual` NotAttending
True
*Main> makeMessage bob bobsResponse
"Sorry, Bob Vance can't attend your event."
But we still can't use ==
with our data type. That'd be a lot better than using invitationResponseIsEqual
. Think back to the chapter on typeclasses. Do you remember the Eq
typeclass? It contains the functions ==
and /=
. To be able to use those, we have to somehow declare that InvitationResponse
is an instance of the Eq
typeclass. To do that, we need to define ==
!marginnote(We get '/=' for free because Eq
has it defined as x /= y = not (x == y)
by default, so once you have ==
Haskell can define /=
on its own.). Here's how we do that.
-- invitationResponseFunctionsBetter.hs
!include(haskelltests/should_compile/invitationResponseFunctionsBetter.hs)
Let's take a closer look at what's going on there.
instance Eq InvitationResponse where
-- [1] [2] [3] [4]
-
instance
: This is how we tell Haskell that we're saying a type is an instance of a typeclass -
Eq
: This is the typeclass we're makingInvitationResponse
an instance of. -
InvitationResponse
: This is the typeclass we're making an instance ofEq
. -
where
: This is the samewhere
you see in function definitions, it means there's going to be bindings soon (bindings are things likei = 3
, they're values bound to names).
After instance Eq InvitationResponse where
, we have our definition of ==
. Let's can import this and test it out, just to prove it works.
*Main> :l invitationResponseFunctionsBetter.hs
[1 of 1] Compiling Main ( invitationResponseFunctionsBetter.hs, interpreted )
Ok, one module loaded.
*Main> Attending == Attending
True
But this is bad. It's a lot of repetitive typing you'd have to do every time. In software this is called boilerplate. Fortunately, Haskell has a built-in way to automatically derive Eq
for us, if we tell it to, using the deriving
keyword!sidenote(Imagine if you had to do that manually for every data type you made! Haskell gets some criticism of being "for academics", but actually it's usually quite practical.). Let's redefine InvitationResponse
to use it.
Prelude> data InvitationResponse = Attending | NotAttending | MightAttend deriving (Eq)
Prelude> Attending == Attending
True
That's much better. We should also add Show
and Read
, because they make sense here!sidenote(By default, the typeclasses you can automatically derive are Eq
, Ord
, Enum
, Bounded
, Show
, and Read
.).
Prelude> data InvitationResponse = Attending | NotAttending | MightAttend deriving (Eq, Show, Read)
Prelude> show Attending
"Attending"
Prelude> read "Attending" :: InvitationResponse
Attending
!newthought(Some of) our data types need to be more complex. Sometimes they need to contain other values. This seems strange, but will be clear after a quick demonstration.
Prelude> data Color = White | Black | Red | Green | Blue deriving (Eq, Show)
Prelude> data Residence = House Color | Apartment deriving (Eq, Show)
Prelude> House White
House White
We define two data types here. The first is the type Color
, which can have the value White
, Black
, Red
, Green
, or Blue
. We also make the type Residence
, which can either be a House
or an Apartment
. In addition, if it's a House
, it contains a value of type Color
!sidenote(This means any time we have a House
, it also has to have an associated Color
.). Let's see this in action:
Prelude> data Color = White | Black | Red | Green | Blue deriving (Eq, Show)
Prelude> data Residence = House Color | Apartment deriving (Eq, Show)
-- My friend lives in an apartment.
Prelude> myFriendsResidence = Apartment
-- I painted my house green, because it's my favorite color.
Prelude> myFavoriteColor = Green
Prelude> myResidence = House myFavoriteColor
Prelude> myResidence
House Green
Prelude> myFriendsResidence
Apartment
-- We can even have a little fun with it.
Prelude> potusHouse = House White
-- myResidence, myFriendsResidence, and potusHouse are all of type Residence
Prelude> :t myResidence
myResidence :: Residence
!newthought(This seems) like a good time to introduce some new terminology.
-
Type constructors: When you write
data Color = [...]
ordata Residence = [...]
, that's you declaringColor
orResidence
as a type constructor!sidenote(In this case, a type constructor that takes no parameters. Since it takes no parameters, we usually just call it a type. We'll discuss type constructors that take parameters later in this chapter.). Anywhere that Haskell expects a type, you can pass itColor
orResidence
. For example, if you had a function calledhouseAppraisal
that takes aResidence
and returns a number representing its value, it's type signature could behouseAppraisal :: (Num a) => Residence -> a
.Residence
here is a type constructor, but we'd usually just call it a type. -
Data constructors: On the other hand, when you write
White | Black | Red | Green | Blue
orHome Color | Apartment
, you're defining data constructors. These are functions and can be used in expressions or pattern matching. For example, the functionwarmInSummer (Home Black) = True
. Here,Home
is a data constructor that takes one argument!marginnote(A data constructor that takes one argument is called a unary data constructor.), andApartment
is a data constructor that doesn't take any arguments either.!marginnote(Data constructors are sometimes also called value constructors.).
Data constructors can take as many data parameters as you want. Let's write a SongInfo
data type which stores a Song
, with a String
for the name of the song, a String
for the artist, and a Float
for the length of the song.
Prelude> data SongInfo = Song String String Float deriving (Show)
Prelude> Song "Dirty Diana" "Michael Jackson" 4.47
Song "Dirty Diana" "Michael Jackson" 4.47
Exercises:
-
Write a data type that has the possible values
A
,B
,C
,D
, andNone
, that you might use to store the user's answer if you were writing a program to administer multiple choice tests. -
Write some data types used to store information about a car. You should have one data type for
Make
(Honda
,Toyota
,GM
, orTesla
) and one forEngineCylinders
(Four
,Six
, andN/A
). Then make a data typeCarInfo
which stores theMake
and theEngineCylinders
, plus aString
representing the name of the owner. Don't forget to addderiving (Show)
! This may seem difficult but once you've done it, it won't seem bad at all.
!newthought(Since type) constructors and data constructors are always used in different places, Haskell allows you to have a data constructor with the same name as a type constructor. Take this somewhat useless type:
data Bob = Bob
This makes a new data type, Bob
, with one possible value, Bob
. There's not really a good reason to make this type, but Haskell will still allow you to. Bob
the data constructor takes no arguments, so it is sometimes called nullary or constant. Compare it to House
earlier, which took a Color
as an argument.
You can also use regular types as parameters. Let's make a Person
type, with a String
for his name.
Prelude> data Person = Person String deriving (Eq, Show)
Prelude> Person "Andy"
Person "Andy"
It'd also be useful to have a function that can extract the name from a Person
. Let's write one using pattern matching.
Prelude> getName (Person name) = name
Prelude> getName (Person "andy")
"andy"
But we don't actually have to write the getName
function yourself, that would be a bit silly. Haskell has a way for you to write a name for each parameter represents, and create the necessary functions automatically. This is called record syntax.
Prelude> data Person = Person { firstName :: String, lastName :: String } deriving (Eq, Show)
Prelude> jDoe = Person "John" "Doe"
Prelude> jDoe
Person {firstName = "John", lastName = "Doe"}
Prelude> firstName jDoe
"John"
Prelude> lastName jDoe
"Doe"
Let's break down record syntax.
Person { firstName :: String, lastName :: String }
-- [1] [2] [3] [4] [5] [6] [7]
-
Person
: You can think of this as the name of this specific data constructor!sidenote(Which of course, is exactly what it is. A data constructor is a function and this one's name isPerson
. Data constructors are the exception to the rule that functions must begin with a lowercase letter, as they must also begin with an uppercase letter.). -
{
: This is to indicate that we're annotating the types for this data constructor with names using record syntax. -
firstName
: This is the name of the first parameter to the data constructor. -
String
: This is the type that corresponds to the namefirstName
. -
lastName
: This is the name of the second parameter to the data constructor. -
String
: This is the type that corresponds to the namelastName
. -
}
: This indicates we're done with this data constructor and record syntax.
Haskell then automatically makes firstName
and lastName
functions, which take a Person
and return that value!sidenote(Again, this is purely for convenience, as you could write all these functions yourself.).
You'll notice that firstName :: String
and lastName :: String
are just regular type signatures. But be careful, firstName :: String
isn't the type signature of the firstName
function that's created, it's the type signature of what that function returns (in our case, a String
)!marginnote(Note that you can't have multiple records with the same name, even in different data types. If this irritates you, in a later chapter we'll see a GHC feature that loosens this restriction.).
a * * b 3 * 4
!newthought(The) type
keyword makes an alias of a data type!sidenote(also called a type synonym). That just means it takes a data type and makes a new name for it. Remember how instead of writing [Char]
, you can write String
? That's done with the type
keyword.
type String = [Char]
It doesn't make a new type, just a new name for an existing type!marginnote(Although unfortunately in error messages GHC will use the original names and not your synonyms.). It's just to make our functions easier to read.
-- Compare this:
Prelude> p1 = (3, 3) :: (Double, Double)
Prelude> p2 = (10, -1) :: (Double, Double)
-- To this:
Prelude> type Point = (Double, Double)
Prelude> p1 = (3, 3) :: Point
Prelude> p2 = (10, -1) :: Point
Regular types can quickly get hard to manage, but with type aliases, that's somewhat avoided.
Prelude> type Point = (Double, Double)
Prelude> type Path = [Point]
Prelude> type Polygon = Path
Prelude> type ComplexPolygon = [Polygon]
!newthought(Some types) aren't really their own types, they're like modifications of other types. Let me explain. In the past, we've always seen data declarations like this:
Prelude> data Color = White | Black | Red | Green | Blue
Prelude> data Residence = House Color | Apartment
Here, House
is a data constructor!sidenote(Which, remember, is a function.) that takes one argument of type Color
. It then returns a value of type Residence
. Apartment
is also a data constructor, which takes no arguments and returns a value of type Residence
. White
, Black
, Red
, Green
, and Blue
are all data constructors that take no parameters and return a value of type Color
.
Prelude> :t House
House :: Color -> Residence
See? House
is a function that takes a color, and returns a Residence
!marginnote(House
on its own is not a residence, but House Green
is.). But type constructors can take parameters, too!marginnote(These are called parameterized types or parameterized type constructors.). Except their parameters aren't values, they're other types! The most common example is probably the type constructor Maybe
. Here's how it's defined:
data Maybe a = Nothing | Just a
Maybe
isn't a proper type on its own, it's just a type constructor. It takes a type as a parameter, and then the a
in Just a
is replaced with that type. Here's an example. Let's say you want to make a function that takes two numbers and divides the first by the second. But there's not really a good answer when the second value is 0
, because you can't divide by zero. You could use Maybe
to help write that function:
-- maybeDivide.hs
!include(haskelltests/should_compile/maybeDivide.hs)
We can load this into GHCi to test it:
Prelude> :l maybeDivide.hs
[1 of 1] Compiling Main ( maybeDivide.hs, interpreted )
Ok, one module loaded.
*Main> maybeDivide 3 4
Just 0.75
*Main> maybeDivide 3 0
Nothing
Maybe
isn't a type, it's a type constructor. Maybe Double
is a type, but you can also call it a concrete type!marginnote(Sometimes people call type constructors like Maybe
types, either by accident or by laziness, but they really mean type constructors.). Let's look at the type of the Just
data constructor.
Prelude> :t Just
Just :: a -> Maybe a
It takes a value of type a
and returns a value of type Maybe a
, where a
is a type variable and can be any type!marginnote(To reiterate, types and values are different things in Haskell. Maybe Double
is a type, suitable for use in type signatures. Just 4.5
is a value of type Maybe Double
. A function whose return type is Maybe Double
could return Just 4.5
.). Let's look at some more types:
Prelude> :t Just "hello"
Just "hello" :: Maybe [Char]
-- Just "hello" returns a value of type Maybe [char].
-- This means Just "hello" can be the return value of any function that returns a Maybe [char]
Prelude> :t Nothing
Nothing :: Maybe a
-- Nothing is a value of type Maybe a, so it can be the return value of any function that returns a Maybe <something>.
Prelude> :t Just 4
Just 4 :: Num a => Maybe a
-- Just 4 returns a Maybe a, where a is any type in the typeclass Num. We could use this for a function that was returning a Float, a Double, an Int, etc.
These are also called parameterized types because their type constructors that take parameters.
!newthought(There's another) very useful parameterized type called Either
. It's used when a function can return two different types, most often for error handling!marginnote(Either
has an advantage over Maybe
when there are multiple reasons an operation can fail, because you can return an error message instead of an uninformative Nothing
.). Here's how it's defined:
data Either a b = Left a | Right b
Let's play with Either in GHCi.
Prelude> Left "hello"
Left "hello"
Prelude> Right True
Right True
Prelude> :t Left "hello"
Left "hello" :: Either [Char] b
Prelude> :t Right True
Right True :: Either a Bool
Parameterized types are hugely important in Haskell, so I want to go over it again here. Left "hello"
returns a value of type Either [Char] b
. That means if you're writing a function, and its type signature says it returns a value of type Either [Char] <anything>
, then you can return Left "hello"
!sidenote(Recalling, of course, that a [Char]
is any String
, like "hello"
.). Either
is a type constructor that takes two Types as arguments - one for the type of the Left
, and one for the type of the Right
. You can think of Either
as the opposite of a 2-tuple - you use either when you want to return either this or that, you use a tuple when you want to return both this and that.
Let's rewrite maybeDivide
to use Either
instead. It will return a String
with an error message on the left if it couldn't do the division, otherwise it will return the result on the right.
-- eitherDivide.hs
!include(haskelltests/should_compile/eitherDivide.hs)
We can load this into GHCi and play with it.
Prelude> :l eitherDivide.hs
[1 of 1] Compiling Main ( eitherDivide.hs, interpreted )
Ok, one module loaded.
*Main> eitherDivide 3 4
Right 0.75
*Main> eitherDivide 3 0
Left "Sorry, can't divide by zero"
Exercises:
-
In
data Either a b = Left a | Right b
, find the type constructor(s), and find the value constructor(s).Really try this, because understanding Haskell data types is important.
Answer:
Either
is the type constructor, which takes two parameters,a
andb
. The two value constructors are Left (which takes ana
) and Right (which takes ab
).
!newthought(Ok, lets) try writing our own parameterized type, it'll represent a shape. We want the user to be able to use any type of number they want in defining our shape, so we'll not specify one. Just for fun, let's also write a function that takes a shape and tells us it's area!sidenote(A better way would be to make a Shape
typeclass, but this works too.).
-- shapeTypes.hs
!include(haskelltests/should_compile/shapeTypes.hs)
Notice the parentheses when pattern matching in the shapeArea
function. They're there to ensure Haskell knows that (Circle (x, y) radius)
is all supposed to be one pattern, so Haskell knows that radius
is part of Circle
and isn't just another parameter. The same goes for (Rectangle (x1, y1) (x2, y2))
. Without the parentheses, this code wouldn't compile.
Let's load this into GHCi and play with it.
Prelude> :l shapeTypes.hs
[1 of 1] Compiling Main ( shapeTypes.hs, interpreted )
Ok, one module loaded.
*Main> myShape = Circle (2, 2) 5
*Main> shapeArea myShape
39.25
Yep, looks like it's working well! To really drive the point home, let's look at the type for the data constructor Circle
.
*Main> :t Circle
Circle :: (a, a) -> a -> Shape a
So Circle
is a data constructor that takes a tuple of two values of type a
, and another value of type a
, and returns a value of type Shape a
.
You might have noticed that in our definition for Shape
we never say the type a
has to be in the typeclass Num
. We could have, but it's considered bad practice in Haskell. That's because we'd have to duplicate the (Num a) =>
in every function that we made take a Shape a
, even if it wasn't relevant for that function. By not adding it, we have the option to only put (Num a) =>
on the functions that really need it!sidenote(In fact, putting type constraints in data
definitions is considered such bad practice that there are ways to make GHC warn you when you do it.).
You can also parameterize type aliases. For example, we used (a,a)
a few times in that definition - let's try and make that clearer with type aliases.
type Point a = (a, a)
data Shape a = Circle (Point a) a | Rectangle (Point a) (Point a) deriving (Show)
In fact, let's use record syntax to make it even clearer.
*Main> type Point a = (a, a)
*Main> data Shape a = Circle {center :: Point a, radius :: a} | Rectangle {corner1 :: Point a, corner2 :: Point a} deriving (Show)
*Main> myShape = Circle (3,5) 10
*Main> radius myShape
10
This could now be a production-ready shape class!
!newthought(Let's try) and make this clearer by talking about kinds. We discussed earlier that there are two kinds of types. There are concrete types like Int
or [Char]
or Maybe Int
or (Num a) => Maybe a
, and not-real-types like Maybe
and Either
or even Either Int
!sidenote(Remember, Either
needs two values, such as Either Int Int
. If you only provide one, you haven't got a concrete type.). The type constructors in the second group still need something else to turn into a concrete type that we'd be allowed to use in a type signature. Int
and Maybe Int
are concrete types - Maybe
still needs another type to make it a concrete type, so you can't use it in a type signature. We can see this by using the :kind
or :k
!marginnote(Most GHCi commands let you just use the first letter instead of typing out the whole word, like :load
and :l
or :type
and :t
.) commands in GHCi.
Prelude> :k Int
Int :: *
The *
means it's a concrete type!marginnote(In the future, *
will be renamed to Type
. If you're using GHCi and seeing Type
instead of *
, it means you may be reading an out-of-date version of this book.). You could pronounce it like "star" or just "concrete type". Let's look at some other concrete types.
Prelude> :k [Char]
[Char] :: *
Prelude> :k Maybe Int
Maybe Int :: *
See? They're all concrete types.
Let's look at some type constructors instead, like Maybe
.
:k Maybe
Maybe :: * -> *
:k Maybe Int
Maybe Int :: *
Ah, see? Maybe
needs another type as a parameter before it can be a concrete type. You can read this as "The type constructor Maybe takes a concrete type and returns a concrete type". Let's look at Either
.
Prelude> :k Either
Either :: * -> * -> * -- Either is a type constructor that takes two concrete types and returns a concrete type
Prelude> :k Either String
Either String :: * -> * -- Through Currying, we can see the value of Either String. It still needs another concrete type
Prelude> :k Either String Double
Either String Double :: * -- Either String Double is finally a concrete type that we could use in a type signature!
When Haskell wants a concrete type, any concrete type will work, even one made from other type constructors.
Prelude> :k Maybe (Either String Double)
Maybe (Either String Double) :: *
Either String Double
is a *
!marginnote(Remember, *
means concrete type.), and Maybe
takes a *
and returns a *
, so Maybe (Either String Double)
is a concrete type. You may have noticed that lists seem like they're just a modification of another type, like how an [Int]
is just a List of Int
s. Well, []
is actually a type constructor!
Prelude> :k []
[] :: * -> *
[]
the type constructor is different from []
the data constructor, so don't get them confused. [Int]
is a concrete type appropriate in a type signature (and is actually syntax sugar for [] Int
), [1]
is a value (and is syntax sugar for 1:[]
).
!newthought(But when) is learning about kinds useful? Well, let's talk about the Functor
typeclass. The Functor
typeclass is for things that can be mapped over. You might remember the map
function, which takes a function and a List, and applies the function to everything in the List.
Prelude> map (+5) [1,2,3]
[6,7,8]
Well, for historical reasons map
only works on lists, but there are lots of other things that can be mapped over. In Haskell you can have sets, arrays, fingertrees, and all kinds of other data structures we'll discuss later. These data structures are in the Functor
typeclass. Because map
was stuck as only working on lists for historical reasons!marginnote(You'll find that "historical reasons" explains quite a bit about Haskell.), they made fmap
, which is the same but works on any Functor
. Here's how it's defined.
class Functor f where
fmap :: (a -> b) -> f a -> f b
You might be curious what that f a
is (notice that it's the same f
from Functor f
). It's something we haven't seen before. The f
represents a type constructor, and it later an a
(which can be of any type) and returns a concrete type!marginnote(This means that type constructors can be instances of type classes, not just concrete types. This is very powerful because it lets us make functions that work on all lists, like fmap
, but also works on other type constructors, like Maybe
(which we'll see in a moment).)! So Functor
is a typeclass that applies to type constructors, not complete types. []
is an instance of Functor
, of course. Let's see how that's defined.
instance Functor [] where
fmap = map
Pretty basic. fmap
is just the functor typeclass version of the List-specific map
. When applied to lists, fmap
only needs to do what map
already does. Let's try applying it to Maybe
. fmap (+1) (Just 4)
evaluates to Just 5
. fmap (+1) Nothing
evaluates to Nothing
. If you're mapping over Just <something>
you apply the function to <something>
, if it's Nothing
you return Nothing
. Here's it in Haskell.
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
Let's try it out.
Prelude> fmap (\x -> x * 2 + 1) (Just 1)
Just 3
Prelude> fmap (\x -> x * 2 + 1) (Nothing)
Nothing
You can probably see how this would be useful! Imagine we had a function that returned a Maybe Int
, and we wanted to do some calculations on the Int
inside. We can do them all with fmap
s, and wouldn't have to first check if it was really a Nothing
.
Exercises:
-
Do you remember the
foldr
function? We used it to sum a list withsum xs = foldr (\x acc -> acc + x) 0 xs
. Here is its type:Prelude> :t foldr foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
What does the type variable
t
take when folding over a list? Hint: look at the third argument offoldr (\x acc -> acc + x) 0 xs
.
!newthought(As you) know, we have access to quite a few typeclasses in Haskell, such as Eq
, Ord
, Show
, and Num
. Let's see how Eq
`might be defined!marginnote(Note that this is not the full definition because it lacks the default implementations, which we'll add in a moment.):
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
Let's zoom in on this, although it's pretty simple.
class Eq a where
-- [1] [2] [3]
-
class
- This keyword tells Haskell that we're declaring a new typeclass. -
Eq a
- Then comes the name of the typeclass that we're defining, and the type variable that we'll be using. -
where
- This keyword should look familiar! It means we're going to be defining bindings soon. Here though, all we've done is declare functions and give their type signature - we don't actually give the definition of the function.
To make a data type an instance of this typeclass, we'd have to use the instance
keyword!sidenote(see Basic data types for more). To make a type an instance of a typeclass, a definition for every function declared in the typeclass must be given, unless there is a default implementation. This is done in the Eq
typeclass, like this.
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
Of course, you must override one of these, otherwise you will find yourself in an infinite loop. Remember the Show
typeclass? It contains a function, show
!marginnote(Show
also has some other functions that aren't relevant here, like showsPrec
.), which takes a value and returns a String
which is supposed to represent the value somehow. Let's write our own, ShowFrench
, which will be similar, but return a representation in French instead of English.
-- showFrench.hs
!include(haskelltests/should_compile/showFrench.hs)
Now, let's load this into GHCi to test it out!
Prelude> :l showFrench.hs
[1 of 1] Compiling Main ( showFrench.hs, interpreted )
Ok, one module loaded.
*Main> showFrench (1 :: Int)
"un"
*Main> showFrench True
"vrai"
Seems to work, but if you pass showFrench
an Int
greater than 3
, your program will crash!marginnote(Specifically, what will happen is an exception will be triggered, and it probably won't be handled because we haven't learned how to handle exceptions yet.). In those cases, it would be nice to just show the number. Let's use this opportunity to also require that instances of ShowFrench
must also be instances of Show
, and then just run show
on the number if we don't have a French translation. Here's how we'd do that.
-- showFrenchBetter.hs
!include(haskelltests/should_compile/showFrenchBetter.hs)
Notice how we changed ShowFrench a
to (Show a) => ShowFrench a
. This means that our type variable a
must be an instance of Show
.
!newthought(We've used) a little bit of recursion in previous chapters, but it's very important!marginnote(Haskell actually doesn't have the primitive while
or for
loops you might be familiar with from other languages. All looping behavior must be accomplished with recursion at some level.) so I'd like to get some practice in.
-
Let's write a copy of the
length
function!marginnote(In case you've forgotten,length
is a function that takes a List and returns its length.). Now, let's think about how we would write this recursively.A strategy tip for writing recursive programs is to think about what case is the most basic. For us, it's probably the empty List, which obviously has length zero. Here's how it's done.
-- length.hs
!indent(!include(haskelltests/should_compile/length.hs)) ```
This may seem like it's less efficient than a more iterative approach like you'd see in other programming languages. But GHC does something called *tail call optimization*, which means that if a function ends in a recursive call, it will be optimized into something more similar to a *while loop* from other languages!sidenote(Provided you have the correct optimization settings turned on in GHC.).
-
Let's write a function that will take a List of numbers, and return a List of all those numbers raised to the second power.
As before, the most basic case for this function is the empty List, because it doesn't have to do anything. We'll also be using the classic
:
operator, which takes a value of types
and a List of type[a]
, and returns a new List with that value at the beginning. So1:[2,3]
evaluates to[1,2,3]
, and1:[]
evaluates to[1]
!marginnote(You may remember that[1,2,3,4]
is actually syntax sugar for1:2:3:4:[]
, so saying "1:[]
evaluates to[1]
" is a bit of a simplification.).-- squares.hs
!indent(!include(haskelltests/should_compile/squares.hs)) ```
-
Let's write
map :: (a -> b) -> [a] -> [b]
. If you've forgotten,map
takes a function and a List, and applies the function to every element in the List. Somap (+1) [1,10,20]
evaluates to[2,11,21]
You'll notice that the most basic case (the base case, if you will) is usually the empty List, for functions that operate on lists. Here's how we'd write this:
-- map.hs
!indent(!include(haskelltests/should_compile/map.hs)) ```
Study this until you grok how it works - it's very similar to the `map` function we wrote earlier.
-
Let's write a function that iterates over a List of numbers and removes all the even numbers, we'll call it
odds
.To aid with this, we'll use the
rem
function, which divides two numbers and tells us the remainder!sidenote(We could also use themod
function, which is similar, butrem
is faster.). If a number is odd, it's remainder won't be0
!sidenote(It will either be1
or-1
.).rem
is only available forIntegral
, notNum
, so we'll have to limit ourselves to that.-- odds.hs
!indent(!include(haskelltests/should_compile/odds.hs)) ```
What this does is check if the first element in the List is odd, and if so, combine it with `odds` called on the rest of the list. Otherwise, we'll just return `odds` called on the rest of the List. And as usual, if the List is empty, we return the empty List.
Exercises:
-
Write the
filter :: (a -> Bool) -> [a] -> [a]
function, call itfilter'
. It takes a function of type(a -> Bool)
and a List of type[a]
, and then filters the List by removing the elements where the function returnsfalse
when that element is passed into it. For example,Prelude> filter (\x -> x > 5) [1,2,3,4,5,6,7,8,9,10,-20,400] [6,7,8,9,10,400]
Hint: Look at our
squares
function and ourmap'
function. -
Rewrite your filter function using guards.
!quickref(newtype
is used when you want to wrap one type in another type and nothing more complicated than that. Because of this restriction, it can be represented the same as the original type in memory, meaning there is zero runtime penalty for using a newtype.)
newtype Dollars = Dollars Int
Here, newtype is being used to take the somewhat uninformative type Int
and create a more descriptive type, Dollars
. To make a value of Dollars
, one might write Dollars 3
.)
Sometimes you want to make a type that's almost the same as another type. For example imagine our program calls for a Dollar
type, a Yen
type, and a Euro
type, which are all just wrappers around Double
. And let's say also we had a Currency
typeclass with a convertToDollars
and convertFromDollars
function. We'd like to add, subtract, and multiply our currency like we could regular numbers.
One way to make our types would be as follows:
!include(haskelltests/should_compile/currency_basic_types.hs)
Now, this has two issues. The first is that we can't add or subtract two Dollars
. We'd have to make each of these an instance of the Num
typeclass, like this.
!include(haskelltests/should_compile/currency_with_instance.hs)
This is some pretty boring boilerplate - it'd be almost the exact same for every currency! Wrapping one type (in our case, Double
) in another (in our case, Dollar
) is such a common need that we have special syntax for it, newtype
.
!include(haskelltests/should_compile/currency_with_newtype.hs)
The main difference between using newtype
and data
is that newtype
only works with the very simple situation of wrapping one type in one other type. You can't use sum types or have multiple types wrapped up in one. And there's a special GHC feature that makes newtype
much more useful by letting it automatically derive typeclasses for you, and you turn it on by putting {-# LANGUAGE GeneralizedNewtypeDeriving #-}
at the top of your code. This is called a pragma to turn on a language extension. We'll discuss these later, for now just know that putting that at the top of a file turns on an extra feature of GHC. Here's how it looks in action:
!include(haskelltests/should_compile/currency_with_newtype_derived.hs)
With GeneralizedNewtypeDeriving
turned on, we were able to add Num
to our list of typeclasses we'd like to be automatically derived, which is very useful! We'd be able to run (Dollar 3) + (Dollar 4)
to get Dollar 7.0
.
There's one other difference between newtype
and data
. Specifically, whether the constructor is strict or lazy. Imagine the following:
data D = D Int
newtype N = N Int
Now, you may remember that Haskell tries to only evaluate things when really necessary, so if you write 1+2
it won't actually evaluate that until it needs to. Haskell also has a special value named undefined
which you can pass to any function and causes your program to instantly crash when it's evaluated.
Prelude> undefined
-- Error!
But, it won't evaluate it until it has to.
Prelude> a = undefined
-- Works fine, until we try to use a
You can even pass it to a function, and if the function doesn't look at it your program will still run.
Prelude> a = undefined
Prelude> silly v = "silly"
Prelude> silly a
"silly"
Now, we'd like our Dollar
to function identically to a Double
. But if we defined it with data
, it wouldn't! Let's see how.
Prelude> sillyD (Dollar v) = "silly"
Prelude> sillyD undefined
-- error!
We could run silly undefined
without crashing, but we can't for sillyD undefined
! However, this works as expected with newtype
.
Prelude> sillyD (Dollar v) = "silly"
Prelude> sillyD undefined
"silly"
This difference is somewhat technical and academic, it's worth discussing. To reiterate:
-
data
is for making new, complicated types, likedata Person = Bob | Cindy | Sue
. -
newtype
is for "decorating" or making a copy of an existing type, likenewtype Dollar = Dollar Double
. -
type
is for renaming a type, liketype Polygon = [Point]
, which just makesDollar
be equivalent toDouble
and is mostly only used for making certain code easier to read.
!quickref(When writing real programs, you typically use Stack
to actually build your code. Stack calls another program, Cabal, which then calls the haskell compiler GHC.To create a project, one uses stack new <project name>
, which will create a project with some common conveniences like a folder for writing tests. You build your project with stack build
, and run it with stack exec <project-name>
.
Code we write sometimes needs to interact with the outside world. For this, we use something called IO
and the main
function. main
is a special function which is automatically called when your program starts. It has the type IO, which allows it to returns special instructions which are then executed. This is the way Haskell allows side-effects while maintaining purity. Examples include print
which prints any value to the terminal, and putStrLn
which puts a string on the terminal. Multiple IO actions can be chained together with the >>
or >>=
operators, which return themselves an IO action. To save on typing, one can also chain up a large number of IO actions with do
notation, which just uses >>=
internally.)
!newthought(There are) a few things you must be able to do before you can write real Haskell programs. You need to be able to write a program, compile it, and run it. You need to be able to make this program do things that contain the dreaded side-effects - stuff that pure functions can't do, like read files or write text to the terminal. And, most importantly, you need a stylish environment to develop your Haskell in!
!newthought(For developing) Haskell, there are many excellent options. There's VSCode with Haskero (what I personally use), Spacemacs (which is great if you're used to Emacs or Vim), and Emacs with Dante are common options. Extensions also exist for Atom and Sublime Text. I will be assuming you use the same setup as me, VSCode and Haskero.
In VSCode I find the "files.autoSave": "onFocusChange"
setting to be invaluable, as it will mostly keep your files saved without too much trouble This also works well with some GHC options that automatically rebuild your code every time it changes, more on that later. I recommend checking if your favorite text editor has something similar.
First some quick names you might not be familiar with.
-
GHC is a Haskell compiler. It reads your Haskell source code and compiles it into a form readable by a computer. There are many other Haskell compilers, but GHC is the best!marginnote(It's somewhat unfortunate that there's only one popular Haskell compiler, since it's not without issues. But in general it's very nice, especially with all the nonstandard but very useful language extensions it contains.). You got it when you installed the Haskell platform.
-
Cabal is a file format for describing certain information about your project. There's also a tool called Cabal, but we won't be using it. Well, we'll be using Stack, which uses Cabal in the backend. The file you describe your package in is normally called
projectName.cabal
!sidenote(Obviously,projectName
replaced with the name of your project.). -
Stack is a program for managing the packages you use in your project. It is mostly used to aid in using 3rd party code and installing the correct version of GHC. It has a file called
stack.yaml
, but also reads<projectName>.cabal
, and confusingly a file namedpackage.yaml
that overrides<projectName>.cabal
. An important goal of Stack is that if it works on my machine, it should work the exact same way on yours, too. This is called reproducible builds!marginnote(The goal actually is that it creates the exact same binary, so it's guaranteed to run the same way.), and is a very helpful property. You already got Stack when you installed the Haskell platform.
!newthought(Stack contains) many useful features. One is that it allows you to instantly create projects according to some common templates. The one we will use is simple
. In a terminal, navigate to your development directory and run the following.
stack new project1 simple -p "author-name:Andre Popovitch"
This will make a new project called project1
according to the simple
template, and in the licence text it will attribute everything to me!marginnote(There are a number of other parameters you can use as well. They are author-email
, author-name
, category
, copyright
, year
and github-username
.)! Of course, you can change this to your name if you'd prefer I don't have the copyright over all the code in your project. After running this, you'll see a lot of output in the terminal. Hopefully it'll look something like this:
λ stack new project1 simple -p "author-name:Andre Popovitch"
Downloading template "simple" to create project "project1" in project1\ ...
[...]
Selected resolver: lts-12.8
Initialising configuration using resolver: lts-12.8
Total number of user packages considered: 1
Writing configuration to file: project1\stack.yaml
All done.
And here what the contents of the project1
folder should be!marginnote(This uses the cd
command and the ls
command. cd
is short for "change directory", and allows you to navigate to a different folder (in our case, the one we just made). ls
is short for "list", and lists all the files and folders in the folder we're currently in.):
λ cd project1\
λ ls
LICENSE README.md Setup.hs project1.cabal src stack.yaml
!newthought(First on) the List is the LICENSE
file. Open it in your favorite text editor (mine is VSCode) and take a look. If you plan to share your code, this license bears looking at. If you don't care who uses it, the current license is fine. Otherwise, check out ChooseALicense to pick one that works for you. If you don't want anyone to use your code, just replace the contents of this file with the text no license provided, all rights reserved
.
!newthought(Next up) is the README.md
file. This is used by other programmers or possible users of your software to determine what it's purpose is and how to use it!sidenote(It's also the file that will be displayed front-and-center if you ever decide to share your project on Github or Gitlab.). If you intend on sharing your code, it's wise to put some information about your project here. Even if you don't plan on sharing, this can be a useful place to leave notes to your future self.
!newthought(Following that) begins the real meat of our project. Setup.hs
contains only two lines.
-- Setup.hs
!include(haskelltests/projects/project1/Setup.hs)
The purpose of this code is somewhat complicated to explain right now, just know that most projects shouldn't have to modify it or really care about what it does.
!newthought(Next up) is project1.cabal
. Remember the .cabal
files mentioned earlier? This is one of those. Let's see what it contains.
!include(haskelltests/projects/project1/project1.cabal)
Most of this is just data presented in the machine-readable format YAML. so someone could search a database for all projects written by Andre Popovitch
or similar. This is also where we'd add project dependencies.
!newthought(Next is) the src
folder, which only contains one file, Main.hs
. This is where we'd start writing our actual program. Here are the contents of Main.hs
.
-- Main.hs
!include(haskelltests/projects/project1/src/Main.hs)
Every single line of this program contains something we haven't talked about yet. Let's go over them now.
-
module Main where
: This defines a new module calledMain
, which contains the full contents of the file below it. Modules are collections of related code - they're nice because if you break your project into small modules, you can reuse them in future projects or share them with the world!marginnote(There are many sites where you can share your code, but the largest is Hackage. It's often a good idea to search here if you're looking for third-party code, just beware because much of it is somewhat low quality.) for fame and fortune. -
main :: IO ()
: This is the type signature for the functionmain
. Its type isIO ()
, which seems like a very strange type. It would seem to be a function that takes no arguments, and returns a value of typeIO ()
? It won't make complete sense until the next chapter, but for now, remember two things.-
IO
is a type constructor which takes a type!marginnote(You should be familiar with other type constructors from the last chapter. They're things likeMaybe
orEither
, they take one or more types and return a concrete type.). ButIO
is special and known by the Haskell runtime, and it's the magic that allows us to accomplish impure things, like reading and writing files or printing to the terminal. -
()
is the type of a tuple with nothing in it. The tuples we've seen before have all had types like(Int, Bool)
or(Int, Int, Int)
, but they can have one or even no values, like(Int)
or()
. The only tuple of type()
is()
!marginnote(Since the type()
only has one value, it takes no memory at runtime.), which is sometimes pronounced "unit".
-
-
main = do
:main
is a special function that Haskell treats in a different way from all the rest. It's similar tomain
functions in other languages: it's the function Haskell calls for you, so it's where your program begins!marginnote(main
has some other unique properties as well, relating toIO
.). What we have on this line is the first line of a multi-line function.do
is special syntax that we won't cover until the next section, so don't worry about it just yet. In our case, the program would evaluate the exact same without it, so just delete it for now. This is the first change we're making to this project! -
putStrLn "hello world"
: Likeprint
,printf
, orconsole.log
in other languages,putStrLn
is a function that takes some text and writes it to the terminal (with a newline at the end). Now, how can a Haskell function, which is supposed to take some input and return a result based on that input and do nothing else, possibly write to the terminal! And what would it return? Let's examine the type ofputStrLn
.Prelude> :t putStrLn putStrLn :: String -> IO ()
So it takes a value of type String and returns a value of type
IO ()
? Well that makes some sense to the typechecker, sincemain
's type isIO ()
. But what does all thisIO ()
business actually mean? We will explain that very soon, but first let's actually run the project.
!newthought(Now, we) need to run this! A very easy way to test our work is to load it in GHCi. In the root directory of project1
, run stack ghci
. This is similar to running ghci
on its own, but it's a special Stack command that will automatically load the project.
Running stack ghci
, you should see some output then eventually be greeted by a somewhat-familiar prompt.
*Main>
Now, remember our confusing main
function from before? It takes no parameters, so we can just type main
to run it.
*Main> main
hello world
This is how we run our program for testing. Note that this is the first time we've seen something like this! if our main function was just defined as the string "hello world"
, we'd see quotes around it.
*Main> main' = "hello world"
*Main> main'
"hello world"
We can also just write putStrLn "hello world"
to print hello world
, instead of calling main
!marginnote(Remember, putStrLn
takes a string, prints it to the terminal, then returns an IO ()
.).
*Main> putStrLn "hello world"
hello world
Let's change our function from printing a boring hello world
to a more modern Hello, World!
. Just go into src/Main.hs
and change
main =
putStrLn "hello world"
to
main =
putStrLn "Hello, World!"
Then go back to GHCi and type :r
. Short for :reload
, this reloads everything we've loaded into GHCi.
*Main> :r
[1 of 1] Compiling Main ( ...\project1\src\Main.hs, interpreted )
Ok, one module loaded.
*Main> main
Hello, World!
Now, it's time to explain what all this strange IO ()
business represents.
!newthought(This section) provides background for the next but is not required for understanding anything in this book. However it's quite interesting!
Haskell has some important principles. First, purity, which includes referential transparency. It means if you call a function, and the function returns 3
, that's the same as if you just wrote 3
instead of calling the function with those parameters. Functions in Haskell must always return the same value for the same parameters. The other restriction, which is closely related, is a lack of side effects. This means functions can't do anything besides return a result. They can't write a file, open a browser, draw to the screen, etc. This provides Haskell with an issue: how is it supposed to do anything productive if functions can't do anything besides return values? We couldn't print to the terminal, draw on the screen, or do anything interesting.
This conundrum is resolved with two things: the main
function, and the IO
type constructor.
main
is a special Haskell function. Your main
function will be automatically executed by Haskell, and it should be of type IO ()
. Haskell will evaluate this function, and execute all the IO actions returned by main
. An example of an IO action is the output of putStrLn "hello"
. It contains some instructions to Haskell telling it to print hello
. IO
and main
are magic in Haskell. That means they're special constructs that let you do things that would be impossible otherwise!marginnote(On the other hand, do
is not magic. It's just special syntax, which is very helpful but we won't be using it just yet. IO
is somewhat complicated, and it's easier if you don't have strange magic-seeming syntax confusing you.)
!newthought(Here's why) we need IO
. Let's say we had a function that was very un-Haskell-like. Imagine a hypothetical function readFromFoo
. The first time you call it, it will open a file Foo.txt
and return the first character. The second time, it will return the second character. The third time, it will return the third character, etc. readFromFoo
is not a real function, so this won't work, but here's how it might look in GHCi.
!marginfigure(readFromFoo
is impure - it does not have the same output for the same input (in this case, no input).)(/static/wise_mans_haskell/assets/exp_impurity_demo.svg)
Prelude> :t readFromFoo
readFromFoo :: Char
-- For example purposes, let's say `Foo.txt` contains `ABC123`.
Prelude> readFromFoo
'A'
Prelude> readFromFoo
'B'
Prelude> readFromFoo
'C'
Prelude> readFromFoo
'1'
Prelude> readFromFoo
'2'
Prelude> readFromFoo
'3'
This is an impure function. As you can see, every time we call it, its input stays the same (we don't pass it anything), but its output changes. This is bad because GHC assumes all functions are pure. This means that it will assume that if it evaluated readFromFoo
once, it won't have to evaluate it again, and can just re-use the same result from last time. So our code might actually end up looking like this!
Prelude> readFromFoo
'A'
Prelude> readFromFoo
'A'
Prelude> readFromFoo
'A'
[...]
This is obviously not what we intended!marginnote(The issue, really, is that GHC assumes functions are pure and does optimizations based on that assumption. If that assumption is not correct, then GHC's attempts at optimization can break our code! This section discusses how we can force GHC to not optimize our impure functions.). But that's okay, this is easily fixed. GHC will only try to reuse the old results if the parameters were the same. So we can just add a parameter that does nothing, just to keep GHC from re-using old results, and the output will be what we want.
Prelude> readFromFoo 17281
'A'
Prelude> readFromFoo 387
'B'
Prelude> readFromFoo 2811002
'C'
I just made these numbers by typing randomly on my keyboard. But this seems kind of inelegant - having to spam your keyboard every time we want to read from our file. But there's an even bigger issue that this doesn't even fix!
Haskell provides very few guarantees about the order you can expect evaluation to happen. We only know one function a
will be called before function b
if the result of a
is a parameter to b
!marginnote(This is called dependency injection.). For example, in foo (bar x)
, bar
must be evaluated before foo
, because an output of bar
is an input of foo
.
That's not the case here, so we don't know what order evaluation will happen in! Consider the following function:
readFromFooTwice = [first, second]
where first = readFromFoo 2423
second = readFromFoo 414
The random numbers keep Haskell from re-using the value of readFromFoo
. But, GHC would be free to evaluate the two readFromFoo
s in any order it wanted! If we're lucky we could get first
evaluated first, and second
evaluated second, in which case readFromFooTwice
would return "AB"
!marginnote(Remember, calling readFromFoo
6 times is supposed to return 'A'
, 'B'
, 'C'
, '1'
, '2'
, '3'
.). But they could just as easily be evaluated in the opposite order, leaving us with "BA"
! To fix this, let's change what readFromFoo
actually does. Now, instead of just returning a Char
, it'll return a tuple of type (Char, Int)
. The Int
is just whatever Int
we passed in, incremented by one.
Prelude> readFromFoo =
('A', 1)
Prelude> readFromFoo 387
('B', 388)
!marginfigure(The changes made to our readFromFoo
function)(/static/wise_mans_haskell/assets/exp_readFromFoo_comparison_1.svg)
Then, instead of just passing in random values, we'll pass the result of the first as an input to the second! Let's rewrite our fake-function readFromFooTwice
.
readFromFooTwice = [first, second] where
(first, int1) = readFromFoo 2423
(second, int2) = readFromFoo int1
Now, GHC will have to evaluate the readFromFoo 2423
before it can evaluate readFromFoo int1
, because it doesn't know what int1
will be yet! You'd think the problem is solved, this is really only a band-aid. Let's say we wanted to call readFromFooTwice
twice.
readFromFooFourTimes = oneAndTwo <> threeAndFour where
oneAndTwo = readFromFooTwice
threeAndFour = readFromFooTwice
Just like before, GHC could run oneAndTwo
after it ran threeAndFour
! There's nothing that compels it to run oneAndTwo
first. And worse, because we do readFromFoo 2423
in readFromFooTwice
, there's nothing that stops Haskell from reusing the output, even though that isn't what we want. What we wanted was:
Prelude> readFromFooFourTimes
"abc1"
But we might end up with
Prelude> readFromFooFourTimes
"c1ab" -- or worse, "abab"!
Do you remember how we modified readFromFoo
to return the number that was passed to it, incremented by one? We can solve the issue with readFromFooTwice
the same way. Let's modify readFromFooTwice
to take an Int
, and return a tuple with the result and the extra value.
readFromFooTwice int0 = ([first, second], int2) where -- notice the new parameter, int0
(first, int1) = readFromFoo int0 -- we use the new parameter instead of a hardcoded number.
(second, int2) = readFromFoo int1
Now, we can modify readFromFooTwice
so it takes a number and uses that instead of using the name number every time. Let's rewrite readFromFooFourTimes
.
readFromFooFourTimes int0 = (oneAndTwo <> threeAndFour, int1) where
(oneAndTwo, int1) = readFromFooTwice int0
(threeAndFour, int2) = readFromFooTwice int1
Let's look at the flow of Int
s here. readFromFooFourTimes
takes int0
and passes it to the first readFromFooTwice
. Internally it goes into readFromFoo
, comes out, goes into the next readFromFoo
, and comes out of our readFromFooTwice
function as int1
. We then pass it to the second readFromTwice
, where the process repeats.
You'll notice that, if we chose this style to represent our impurity, every function that touches impurity would have to return its result decorated with an extra Int
. Any function that calls an impure function is "corrupted" by their impurity, and must do the dance of passing Int
s around. Without too much effort, we've forced Haskell to never evaluate our functions out of order and never re-use previous values. Of course, we don't have to do all this when writing real Haskell. Haskell has a way to handle this all for us, and we don't even need to be aware it's doing it.
!newthought(You can) think of an IO action, like the result of putStrLn "hello world"
, as a piece of data that contains all the information necessary to execute that action. Whatever that action would return, that's the type of a
in IO a
. That's why putStrLn
returns IO ()
. It doesn't really make sense for it to return anything, so ()
is used to signify that - there's only one possible value of type ()
, so a function that returns type ()
means it doesn't tell you any new information.
Whatever IO
action gets returned by main
, Haskell takes it and executes it. This is another thing that makes main
special!marginnote(Also, remember how we could write a value in GHCi and it would be evaluated, then the result would be printed? Well, there's an exception to that. If it's an IO a
function, GHCi will execute the IO action inside. If one of those actions is writing to the terminal (as is the case of putStrLn
), that's just what GHCi will do. Then, it'll look at the type a
, the return type of the IO action we just performed. If it's not ()
, GHCi will print the return value. You'll see that when we introduce an IO
function which isn't IO ()
.).
Let's write a simple IO ()
function. All it will do is write "helloworld" to the terminal.
Prelude> helloWorld = putStrLn "helloworld"
Prelude> helloWorld
helloworld
Prelude> helloWorld
helloworld
You can think of putStrLn "helloworld"
as returning a note that says "Print helloworld
to the terminal". When GHCi sees the note, it follows the instruction and prints helloworld
. Now, you can't run show
on an IO
value, so we can't really see inside. But, like our readFromFoo
example above, The tricky part is you can't just throw IO
values around, they have to be called from another IO
function. To perform multiple IO actions in one function, we chain them together with the >>
function. It returns a new IO action with the type of the last action in the chain.
Here's how we'd chain up two putStrLn "helloworld"
s.
Prelude> helloWorldDouble = putStrLn "helloworld1" >> putStrLn "helloworld2"
Prelude> helloWorldDouble
helloworld1
helloworld2
Let's try it some more.
-- Let's also examine the type of (putStrLn "helloworld1" >>)
Prelude> :t (putStrLn "helloworld2" >>)
(putStrLn "helloworld2" >>) :: IO b -> IO b
-- So (putStrLn "helloworld2" >>) is a function that
-- takes a value of type IO b and returns an IO b.
-- Whatever (putStrLn "helloworld2") returns is essentially thrown away.
-- Let's see what the type is when we provide it with a second argument.
Prelude> a = putStrLn "helloworld2" >> putStrLn "helloworld3"
Prelude> :t a
a :: IO ()
-- So putStrLn "helloworld2" >> putStrLn "helloworld3" returns something of type IO ().
-- That means we can pass it to >> again, like this
Prelude> helloWorldTriple = putStrLn "helloworld1" >> (putStrLn "helloworld2" >> putStrLn "helloworld3")
Prelude> helloWorldTriple
helloworld1
helloworld2
helloworld3
Like all Haskell functions, putStrLn "helloworld1"
is totally pure. We just cheat because IO
types contain some instructions to do possibly-impure things, and GHCi executes those instructions when it sees them.
If you didn't understand everything that went on here, that's okay. Just remember: we can chain IO a
actions together with >>
, and main
is a function that's automatically called so your program starts.
!newthought(Now, that's) well and good, but what if we want to do some IO action based on the value of another IO action? To explain this we'll be using lambdas, so let's refresh our memory of how they work.
-- Create an anonymous function that takes a value, then applies show to it, then applies putStrLn to that, then returns the result.
Prelude> myLambda = (\x -> putStrLn . show $ x)
Prelude> myLambda 3
3
Prelude> myLambda "hello"
"hello"
-- Since we call show on the input, the type of whatever we pass in must be an instance of the Show typeclass
-- putStrLn's return type is IO (), and since we return putStrLn . show $ x, our lambda's return type is also IO ().
Prelude> :t myLambda
myLambda :: Show a => a -> IO ()
Now that you've seen >>
, let's talk about the >>=
function, pronounced "bind". Anything you can do with >>
you can also do with >>=
, so >>=
is a bit more popular. Remember how >>
worked?
Prelude> helloWorldDouble = putStrLn "helloworld1" >> putStrLn "helloworld2"
Prelude> helloWorldDouble
helloworld1
helloworld2
It took two values, in our case IO ()
and IO ()
, and chained them together!marginnote(In general though, they wouldn't both have to be IO ()
.). Remember, the a
in type IO a
is the return value for the IO action we're performing. Take, for example, the getLine
function. Here's the type for getLine
.
Prelude> :t getLine
getLine :: IO String
Go into GHCi and play with this. Can you figure it out? Don't forget that if you give GHCi a value of type IO a
, GHCi will execute it and then if a
is not ()
, it'll call show
on it and print the result.
Did you try it?
Prelude> getLine
test
"test"
Prelude> getLine
Woah, Haskell is cool!
"Woah, Haskell is cool!"
Here's the answer. getLine
is an IO function, of course. What it does is prompt the user to enter some characters, then return that those characters inside an IO String
type. This makes sense: whatever the a
is in IO a
is the type of the result of the IO
action, and String
is the natural result type for a function that lets the user input some text into the terminal.
Now, let's see the >>=
operator. Let's redefine helloWorldDouble
to use >>=
instead of >>
.
Prelude> helloWorldDouble = putStrLn "helloworld1" >>= (\_ -> putStrLn "helloworld2")
Prelude> helloWorldDouble
helloworld1
helloworld2
Instead of passing >>=
a plain putStrLn "helloworld2"
, we wrapped it up in a lambda that takes one parameter and does nothing it. The reason we did this is that >>=
has to take a function as it's right-hand argument. What it's actually doing here is taking the output of putStrLn "helloworld1"
(which is ()
) and passing it to the (\x -> putStrLn "helloworld2")
function, which ignores it and returns an IO action that prints helloworld2
to the terminal. However, that's not where >>=
shines. It's most useful in situations where you actually care about the output of an IO action. Let's use it with getLine
to have a little more fun.
-- Let's use a simple function that takes a value and prints it with two exclamation marks after it. Then we'll use that function as the argument to(getLine >>=).
Prelude> beLoud = getLine >>= (\x -> putStrLn (x <> "!!"))
Prelude> beLoud
I like Haskell
I like Haskell!!
You really should play with this, as having a firm understanding of what's going on here is critical. It's running the getLine
function, which returns an IO String
. Then >>=
extracts the String
and passes it to the function \x -> putStrLn (x <> "!!")
. That function takes the string, then returns an IO action that prints it with "!!"
added to the end. Notice that even though getLine
returns an IO String
, the x
is just a regular String
. The bind operator "unwraps" it.
The powerful thing about the >>=
operator is that you can chain it as much as you want. Let's use it to write a function that uses five different IO actions.
Prelude> spongebob = putStrLn "Are you ready, kids?" >>= (\_ -> (getLine >>= (\_ -> (putStrLn "I can't hear you!" >>= (\_ -> (getLine >>= (\_ -> putStrLn "Ohhhhh!")))))))
Prelude> spongebob
Are you ready, kids?
Aye aye, captain
I can't hear you!
Aye aye, captain!
Ohhhhh!
Now, you might have thought that was somewhat of a pain to type into GHCi!marginnote(You are typing all these examples into GHCi, right?). Well luckily for us, Haskell provides something called do
notation to save us some work.
!newthought(Let's look) at that spongebob example again, but this time let's put it in a file.
-- spongebob.hs
!include(haskelltests/should_compile/spongebob.hs)
This is, to put it mildly, a mess. Let's add some newlines to make it slightly more clear what's going on.
-- spongebobNewlines.hs
!include(haskelltests/should_compile/spongebobNewlines.hs)
That's a bit more readable. But we're still typing a lot of repetitive lambdas. Do you know what type spongebob
will return?
Prelude> :t spongebob
spongebob :: IO ()
Whatever is the type of the last element in our huge chain, that's what type the whole thing will return. Since putStrLn "Ohhhhh!"
returns type IO ()
, and it's the last thing that'll get done in our sequence, that's the type of the spongebob
function.
But although this formatting is better, we're forced to write <stuff> >>= \_ -> <stuff>
over and over, which is a bit annoying when we'd rather just be focusing on the logic of our program. That's why Haskell provides do
notation. Let's see how it works
-- spongebobDo.hs
!include(haskelltests/should_compile/spongebobDo.hs)
Look how simple that made it! Remember, this is just syntax sugar. Before this is compiled, do
notation is converted to use >>=
, so Haskell doesn't know or care which you use!marginnote(Lots of beginners think do
is magic that lets you use IO
functions, but it's not. And it actually works on much more than just IO! We'll cover that a bit later.). Anything you can do with do
you can do with >>=
.
spongebob =
putStrLn "Are you ready, kids?" >>= \_ ->
getLine >>= \_ ->
putStrLn "I can't hear you!" >>= \_ ->
getLine >>= \_ ->
putStrLn "Ohhhhh!"
do
does not do anything magic, it's just shorthand for long chains of >>=
. However, our program is kind of boring, because we never use the results of previous operations in our next ones. This is how we might do that, using >>=
.
-- spongebobResults.hs
!include(haskelltests/should_compile/spongebobResults.hs)
Let's try it out!
Prelude> :l spongebobResults.hs
[1 of 1] Compiling Main ( spongebobResults.hs, interpreted )
Ok, one module loaded.
*Main> spongebob
Are you ready, kids?
aye aye, captain
I can't hear you!
aye aye, captain!
Ohhhhh!
Results: You said 'aye aye, captain' the first time, and 'aye aye, captain!' the second time
To do this using do
, there's an extra piece of notation we need. It looks like a backwards arrow: <-
!marginnote(It's totally unrelated to the forward arrow in a lambda (\x ->
).
--- spongebobResultsDo.hs
!include(haskelltests/should_compile/spongebobResultsDo.hs)
This is cool! With first <-
and second <-
, when Haskell is converting our do
syntax to regular >>= \_ ->
syntax, it knows to change the _
to first
or second
, depending. For clarity, we could just always use <-
, like this.
spongebob = do
_ <- putStrLn "Are you ready, kids?"
first <- getLine
_ <- putStrLn "I can't hear you!"
second <- getLine
_ <- putStrLn "Ohhhhh!"
putStrLn ("Results: You said '" <> first <> "' the first time, and '" <> second <> "' the second time")
We can't bind the last one to anything, for two reasons. For one, there would be no point, it's at the end so there would be nowhere to use it!marginnote(when you make a binding in a do
expression, it can only be used later in that expression.). And for two, scroll up and look at spongebobResults.hs
. There's no lambda that comes after the last putStrLn
, so there's nowhere to bind the value!marginnote(It's edge cases like these when it comes in handy to know that do
is just a prettier version of >>=
.).
Note that when using do notation, if the first element is type IO a
, every other element has to be some other IO
type. This would not be allowed:
-- badDo.hs
!include(haskelltests/error_examples/badDo.hs)
(1 + 1) :: Int
is, obviously, an Int
. And since the first thing that was encountered in the do
expression was putStrLn "hello"
(which is an IO
type), it wants everything that follows to be an IO
type!marginnote(Yes, that implies do
works with things other than IO
- it does! We'll cover that later.).
If you want to bind (1 + 1)
to a name inside a do
expression, you need to turn it into an IO
action. For that, we have the pure
function, which takes a value and makes it an IO
type. Observe:
-- goodDo.hs
!include(haskelltests/should_compile/goodDo.hs)
The pure
function takes a value, in our case an Int
, then puts it into an IO
value. So it turns an Int
into an IO Int
. Then the <-
extracts the Int
out of the IO Int
, leaving two
equal to 2
!marginnote(There's another function, return
, which does the same thing as pure
. pure
will work in cases return
won't, so pure
is recommended.). This may seem convoluted, but that's how it's got to be done. In practice it doesn't come up that much because you can always use let
and where
:
-- goodDo2.hs
!include(haskelltests/should_compile/goodDo2.hs)
Now that you understand how to effectively use IO
functions, you need to learn what they are.
-
putStrLn :: String -> IO ()
This function takes a
String
and returns an IO action that will print it to the terminal, with a newline at the end. -
putStr :: String -> IO ()
This function takes a
String
and returns an IO action that will print it to the terminal, without a newline at the end. -
putChar :: Char -> IO ()
Like
putStr
, but takes a single character instead of a string. -
print :: Show a => a -> IO ()
Equivalent to
putStrLn . show
. Takes a valuex
and returns an IO action that'll printshow x
. -
getLine :: IO String
Returns an IO action that will allow the user to type some text into the terminal, then returns that text.
-
openFile :: FilePath -> IOMode -> IO Handle
This is part of the
System.IO
package, which means that in order to use it, you have to writeimport System.IO
at the top of every file that uses it. What it does is take a path to a file (which is just aString
) and anIOMode
(which is eitherReadMode
,WriteMode
,AppendMode
, orReadWriteMode
), and return a "file handle" which is basically a reference to a file. If you opened the file inReadMode
orReadWriteMode
you can get the contents with thehGetContents
function. When you're done with the file, runhClose
on the handle to tell the computer you don't need it anymore. Here's how to read a file:import System.IO main = do handle <- openFile "myFile.txt" ReadMode contents <- hGetContents handle putStr contents hClose handle
Writing a file is more complicated due to Haskell's laziness, so we recommend using either the Pipes or Conduit library for it. We'll discuss how to use Pipes in a later chapter.
-
sequence
This is an interesting function, what it does is take a list of
IO
actions and return an IO action that executes all of them.Prelude> sequence [print "hello", print 3, putStrLn "bonjour"] "hello" 3 bonjour [(),(),()]
Note that it's return type is not
IO ()
, it'sIO [a]
, so GHCi will attempt to print its output. That's what[(),(),()]
is at the bottom. It's output is the output of all the IO actions in the list.I didn't write the type signature for
sequence
because it's a little intimidating, but you're welcome to take a look at it yourself with:t
.
!newthought(This is) a project we're going to use to test some of the skills we've learned so far! We're going to make a type of game called a text adventure game. Here's how it'll look when we're finished:
You awaken in a dark cavern, only the dimmest light illuminating your surroundings.
You try to remember how you got there, but you realize you can't remember anything - not even your own name.
You look around, and see two pinpricks of light in the distance, one to your left and one to your right.
Could they be exits?
1: Probably not, better to sit here and wait for someone to find me.
2: Hmm... I'll go explore the possible exit on my right.
3: I have a good feeling about the light to my left
Choice: 1
Story over!
The first thing to do is to make a new project - let's call it textAdventure
, but you're free to call it whatever you like. This is the command you'd run to start a project!marginnote(you may want to change the name, of course.):
stack new textAdventure simple -p "author-name:Andre Popovitch"
Then, navigate into the textAdventure
directory and run stack ghci
to open an environment where we can run our program. Also, open a text editor and view src/Main.hs
- this is the only file we need for doing our work!marginnote(In general, you'll spend most of your time editing files in the src
directory.).
The first thing to do is think about what we want. We need to be able to show some text, like our You awaken in a dark cavern ...
. Let's make a data type that will hold a piece of text to display to the user, we'll call it Message
because it's a message to the user. If the text is to represent someone talking, we'll store his or her name separately from what they say. Add this to Main.hs
data Message = Speaking {texts :: [String], speaker :: String} | Info {texts :: [String]}
We're using record syntax here - reread Creating New Data Types if you've forgotten how this works.
We also want to be able to display choices to the user. Let's make a data type to store a single choice, and a data type to store our whole adventure.
data Choice = Choice {choice :: String, result :: Adventure}
data Adventure = StoryEnd | StoryMessage Message Adventure | StoryChoice [Choice]
You might notice that Choice
can contain an Adventure
and an Adventure
can contain a Choice
. This makes it a recursive data type. Nesting the data types like this will allow us to have an Adventure as long as we want.
!newthought(Let's write) an IO
function, tellStory
, which will take an Adventure
and recursively!marginnote(Recursive data types often demand recursive functions.) present it to to the user.
tellAdventure :: Adventure -> IO ()
Let's think of the three cases our tellStory
function will have to handle. Whatever Adventure
we pass it, it's either going to be a StoryEnd
, a StoryMessage Message Adventure
, or a StoryChoice [Choice]
. If it's a StoryEnd
, that's it for our story, so we can just write that the story is over and be done.
tellAdventure StoryEnd =
putStrLn "Story over!"
Now, if the Adventure
our function gets is a StoryMessage Message Adventure
, we want to print out the messages, and then continue on with the nested Adventure
.
tellAdventure (StoryMessage message adventure) = do
sequence . map putStrLn . texts $ message
tellAdventure adventure
This is pretty simple. The text gets extracted from the message with texts
, yielding a list of String
s. map putStrLn
converts all of those String
s to IO Actions that print that string. sequence
converts that list of IO Actions to a single IO action which will execute every action in the list. The end result is that every String
inside message
gets printed to the terminal. Then, we call tellAdventure
on the adventure
inside the StoryMessage
to continue the story.
The last thing we need is to do is to show the users a List of choices, allow them to choose one, and then continue with the adventure that's contained within that choice. We'll do this by numbering the choices, use getLine
to let the user enter a number, and continue with the adventure in the corresponding Choice
!marginnote(Remember, each Choice
contains an Adventure
.).
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map choiceToString $ indexedChoices
putStr "Choice: "
userChoiceIndex <- getLine
tellAdventure . result . (choices !!) . (\x -> x-1) . read $ userChoiceIndex
where indexedChoices = zip [1..] choices
choiceToString (i, c) = " " <> (show i) <> ": " <> (choice c)
We use zip
to get a list of tuples of choices and their respective number. Then we convert the choices to strings, which is just some simple string manipulation in the choiceToString
function. We then print all those strings using sequence . map putStrLn
like we did in the last function.
Next, we run userChoiceIndex <- getLine
which allows the user to type in the terminal, and we bind whatever they typed to the name userChoiceIndex
.
The last part of the function is the line:
tellAdventure . result . (choices !!) . (\x -> x-1) . read $ userChoiceIndex
Which turns userChoiceIndex
into a number with read
, subtracts 1
from it, gets the choice
at that index with !!
, gets the Adventure
from that choice with result
, then calls tellAdventure
. Easy as pie!
Here's the whole tellAdventure
function, for those following along:
tellAdventure :: Adventure -> IO ()
tellAdventure StoryEnd =
putStrLn "Story over!"
tellAdventure (StoryMessage message adventure) = do
sequence . map putStrLn . texts $ message
tellAdventure adventure
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map choiceToString $ indexedChoices
putStr "Choice: "
userChoiceIndex <- getLine
tellAdventure . result . (choices !!) . (\x -> x-1) . read $ userChoiceIndex
where indexedChoices = zip [1..] choices
choiceToString (i, c) = " " <> (show i) <> ": " <> (choice c)
Now, we just need to write the Main
function and put it all together.
main :: IO ()
main = do
tellAdventure story
where story = StoryMessage (Info ["You awaken in a dark cavern, only the dimmest light illuminating your surroundings.",
"You try to remember how you got there, but you realize you can't remember anything - not even your own name.",
"You look around, and see two pinpricks of light in the distance, one to your left and one to your right.",
"Could they be exits?"])
(StoryChoice [Choice "Probably not, better to sit here and wait for someone to find me." StoryEnd,
Choice "Hmm... I'll go explore the possible exit on my right." StoryEnd,
Choice "I have a good feeling about the light to my left" StoryEnd])
We define story
, and then call tellAdventure
on it. If you try and extend the definition of story
, be careful to mind your indentation! Haskell can be picky!marginnote(Fortunately, this pickiness tends to make sure your programs are aesthetically pleasing.). This story is very simple - it displays a message, then offers 3 choices, all of which end the story. If you want, you can change StoryEnd
to some other value of type Adventure
, like a StoryMessage
or another StoryChoice
.
!newthought(Recall the) head
function. It takes a List and withdraws the first element from that List. So head [1,2,3]
will return 1
. But what happens if the List is empty? Open GHCi and try to use head []
to get the first value of an empty List.
Prelude> head []
*** Exception: Prelude.head: empty List
This makes some sense - There's no first element of an empty List, so Haskell has no idea what to do. Instead, it throw an exception. If you don't take care, a thrown exception can crash your whole program. Since head
works properly with some inputs and creates an exception with others, it's called a partial function. Functions that never throw an exception, no matter the input, are called total functions.
One way of handling partial functions is to just structure our program so they will never be provided with an input that results in a crash. For example, if you have a function that you know will always return a List with some number of elements, it's safe to call head
on the result of that function. However, this is sometimes tricky. If your function has a bug and accidentally returns an empty List, Haskell's type system will not warn you and your program will crash at runtime!marginnote(A goal of Haskell is to catch as many bugs at compile time as possible.)! Let's see another approach.
-- headMaybe.hs
!include(haskelltests/should_compile/headMaybe.hs)
So what does this do? If you try to pass headMaybe
an empty List, it will return Nothing
. But if you pass it any List that is not empty, it will return Just <first element of the List>
.!marginnote(If you're confused by Maybe
, re-read the section on [parameterized types](Parameterized Types)). So this function is total even though it uses the partial function head
, because it only uses head
in situations where it has no chance of throwing an exception. Let's see it in action.
Prelude> :l headMaybe.hs
[1 of 1] Compiling Main ( headMaybe.hs, interpreted )
Ok, one module loaded.
*Main> headMaybe []
Nothing
*Main> headMaybe [1,2,3]
Just 1
Looks good to me. Do you remember our text adventure project? It had a small issue in it. If the user entered something other than a number when asked for their choice, when we call read
on that value the program will crash because read
can't convert it. Luckily for us, there's another useful function inside the Data.Text
module called readMaybe
. readMaybe
is the same as read, except wrapped up in a Maybe
so it can return Nothing
if the value couldn't be converted.!marginnote(If you remember, a module is a collection of related code. Not all of this code is made available at the start of your program due to name collisions, a concept that will be explained shortly.)
To use a module, you have to import it with the import
keyword at the start of your program. Here's a demonstration. If you recall, in the last section we made this file:
module Main where
data Message = Speaking {texts :: [String], speaker :: String} | Info {texts :: [String]}
data Choice = Choice {choice :: String, result :: Adventure}
data Adventure = StoryEnd | StoryMessage Message Adventure | StoryChoice [Choice]
tellAdventure :: Adventure -> IO ()
tellAdventure StoryEnd =
putStrLn "Story over!"
tellAdventure (StoryMessage message adventure) = do
sequence . map putStrLn . texts $ message
tellAdventure adventure
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map choiceToString $ indexedChoices
putStr "Choice: "
userChoiceIndex <- getLine
tellAdventure . result . (choices !!) . (\x -> x-1) . read $ userChoiceIndex
where indexedChoices = zip [1..] choices
choiceToString (i, c) = " " <> (show i) <> ": " <> (choice c)
main :: IO ()
main = do
tellAdventure story
where story = StoryMessage (Info ["You awaken in a dark cavern, only the dimmest light illuminating your surroundings.",
"You try to remember how you got there, but you realize you can't remember anything - not even your own name.",
"You look around, and see two pinpricks of light in the distance, one to your left and one to your right.",
"Could they be exits?"])
(StoryChoice [Choice "Probably not, better to sit here and wait for someone to find me." StoryEnd,
Choice "Hmm... I'll go explore the possible exit on my right." StoryEnd,
Choice "I have a good feeling about light to my left" StoryEnd])
Look at the first line:
module Main where
This says we're creating a new module named Main
. To import a module, we add an import statement beneath:
module Main where
import Text.Read
This will let us use all the functions in the Text.Read
module!marginnote(If you import a module in your code, then import your code into GHCi, you automatically get access to everything that module imports.). But if we do this and try to compile the project with stack ghci
, we will get the following error:
Ambiguous occurrence `choice'
It could refer to either `Text.Read.choice',
imported from `Text.Read' at [...]\textAdventure\src\Main.hs:2:1-16
(and originally defined in `Text.ParserCombinators.ReadPrec')
or `Main.choice',
defined at [...]\textAdventure\src\Main.hs:6:23
|
19 | sequence . map putStrLn . map (\x -> " " <> (show . fst $ x) <> ": " <> (choice . snd $ x)) $ indexedChoices
| ^^^^^^
Can you figure out the issue? Text.Read
has a function called choice
, and we have a function called choice
, and Haskell doesn't know which to use! You can't have two functions with the same name, because that's confusing to you and confusing to the compiler. What we need to do is not import the choice
function from Text.Read
!marginnote(We could also rename our choice
function, but that's annoying to have to do.). We can do that with the hiding
keyword, like this:
module Main where
import Text.Read hiding (choice)
This will tell Haskell to not import the choice
function from Text.Read
. Since we didn't import it, Haskell won't be tripped up by the value named choice
we use later. But this isn't ideal. We only want the readMaybe
function, not all the functions except choice
. We want to only import what we need!marginnote(This is so we won't accidentally bump into a name collision again. Importing only what you need is usually a good idea.). Here's the syntax for that.
module Main where
import Text.Read (readMaybe)
If we wanted to import multiple functions, we can just separate them by commas, like (function1, function2, etc)
. Here's the type of readMaybe
.
Prelude> :t readMaybe
readMaybe :: Read a => String -> Maybe a
!newthought(Let's write) a function which will ask the user for a choice, and return an (Integral a, Read a) => IO a
with the type the user entered. It needs to be a Read
type because this function only makes sense for types that can be read from String
s. It needs to be Integral
because it wouldn't make sense for the user to enter, say 3.5
. Integral
is a typeclass for numbers with nothing after the decimal point.
getChoice :: (Integral a, Read a) => IO a
getChoice = do
putStr "Choice: "
userChoiceIndex <- getLine
maybeIndex <- pure . readMaybeInt $ userChoiceIndex
case maybeIndex of Nothing -> getChoice
Just index -> pure index
where
readMaybeInt :: (Integral a, Read a) => String -> Maybe a
readMaybeInt = readMaybe
Here's how this function works. In the where
block in the bottom, we define a function called readMaybeInt
. It does the same thing as readMaybe
, except we've added another type restriction on the output, specifically whatever it returns needs to be in the Integral
typeclass as well as in the Read
typeclass. readMaybe
on it's own would happily work on 3
, but it would also work on 3.5
or True
or all sorts of other things we don't want.
Now, let's tackle the do
expression. First we do putStr "Choice: "
. This should hopefully be familiar at this point, it prints the text Choice:
to the terminal without a newline at the end.
Then, we do userChoiceIndex <- getLine
. This lets the user enter some text, and binds that text to the name userChoiceIndex
.
Then, we do:
maybeIndex <- pure . readMaybeInt $ userChoiceIndex
This calls readMaybeInt
on userChoiceIndex
. It then uses the pure
function, which turns the Maybe a
returned by readMaybeInt
into an IO Maybe a
. pure
just takes a value of any type, and returns that value inside an IO
type. In this case, its type signature is return :: a -> IO a
. We do this because every line in our do
block has to be an IO
type, but readMaybeInt
returns a Maybe a
, so the pure
makes it a IO Maybe a
. However, we bind it to maybeIndex
with <-
which removes the IO
wrapping, so maybeIndex
is back to being a Maybe a
. The <-
basically undoes all the work done by pure
!marginnote(Luckily GHC is smart enough to simplify this and not actually do anything.).
The last part of our function is a case
expression. In case you forgot, case
expressions allow us to do pattern matching inside a function. If maybeIndex
is Nothing
, the case
expression evaluates to a recursive call of getChoice
, starting the whole function over. Otherwise, the value is Just index
, and we use return index
to wrap index into an IO
. Remember, the last expression of a do
expression is what the whole thing returns, so this function will return an IO
type that wraps up the index the user entered.
Put this function somewhere in Main.hs
of textAdventure
, and load it into GHCi with stack ghci
. Lets test this function out:
*Main> getChoice
Choice: 3.5
Choice: True
Choice: 3
3
You can see that this getChoice
will continually prompt us for choices until we enter a valid number, and then will return that number. Let's modify tellAdventure (StoryChoice choices)
to use getChoice
. Here's how it is now:
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map choiceToString $ indexedChoices
putStr "Choice: "
userChoiceIndex <- getLine
tellAdventure . result . (choices !!) . (\x -> x-1) . read $ userChoiceIndex
where indexedChoices = zip [1..] choices
choiceToString (i, c) = " " <> (show i) <> ": " <> (choice c)
All we have to do is remove line 3, change getLine
to getChoice
on line 4, and remove . read
on line 5.
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map choiceToString $ indexedChoices
userChoiceIndex <- getChoice
tellAdventure . result . (choices !!) . (\x -> x-1) $ userChoiceIndex
where indexedChoices = zip [1..] choices
choiceToString (i, c) = " " <> (show i) <> ": " <> (choice c)
Type :r
!marginnote(:r
is short for :reload
.) in GHCi to reload the project, and then type main
to test it out! Now our program won't crash when you enter something that isn't a number. However, we still have one issue. You can enter any number, but if you enter -5
or 23839
the program will crash. Let's modify getChoice
so it takes a number which will serve as the maximum number it will allow. We'll also disallow numbers less than zero.
getChoice :: (Integral a, Read a) => a -> IO a -- Our function now takes an argument.
getChoice numberOfOptions = do
putStr "Choice: "
userChoiceIndex <- getLine
maybeIndex <- pure . readMaybeInt $ userChoiceIndex
case maybeIndex of Nothing -> getChoice numberOfOptions -- When calling getChoice recursively, we have to pass the input.
Just index -> if index <= numberOfOptions && index > 0 -- We check if index is inside the range of allowed numbers.
then pure index -- If it is, return it.
else getChoice numberOfOptions -- Otherwise, recurse.
where
readMaybeInt :: (Integral a, Read a) => String -> Maybe a
readMaybeInt = readMaybe
Then, we just modify the third line of the tellAdventure
function that matches the (StoryChoice choices)
pattern to pass the length of the List to getChoice
.
tellAdventure (StoryChoice choices) = do
sequence . map putStrLn . map (\x -> " " <> (show . fst $ x) <> ": " <> (choice . snd $ x)) $ indexedChoices
userChoiceIndex <- getChoice (length choices) -- call getChoice with the length of the List
tellAdventure . result . (choices !!) . (\x -> x-1) $ userChoiceIndex
where indexedChoices = zip [1..] choices
And voilà! We have now removed the two biggest bugs in our program. But it's not very user friendly. If the user enters one
when they meant 1
, we just show Choice:
again. It doesn't give any instruction for what the user should do. Let's make one final change to the getChoice
function. Remember, do
is just syntax sugar for an expression, so we can use it in the where
block!marginnote(We can even use do
expressions inside do
expressions!).
getChoice :: (Integral a, Read a, Show a) => a -> IO a -- We need to call show on the input, so the type a needs to be an instance of the Show typeclass.
getChoice numberOfOptions = do
putStr "Choice: "
userChoiceIndex <- getLine
maybeIndex <- pure . readMaybeInt $ userChoiceIndex
case maybeIndex of Nothing -> displayErrorAndRetry -- Use the new displayErrorAndRetry function.
Just index -> if index <= numberOfOptions && index > 0
then pure index
else displayErrorAndRetry -- Use the new displayErrorAndRetry function.
where
readMaybeInt :: (Integral a, Read a) => String -> Maybe a
readMaybeInt = readMaybe
displayErrorAndRetry = do -- Define a function that prints an error then returns getChoice numberOfOptions.
putStrLn $ "Please enter a number 1 through " <> (show numberOfOptions) <> "."
getChoice numberOfOptions
Now if you reload GHCi with :r
and then run main
, you'll find a friendly error message when you enter an invalid value.
*Main> main
You awaken in a dark cavern, only the dimmest light illuminating your surroundings.
You try to remember how you got there, but you realize you can't remember anything - not even your own name.
You look around, and see two pinpricks of light in the distance, one to your left and one to your right.
Could they be exits?
1: Probably not, better to sit here and wait for someone to find me.
2: Hmm... I'll go explore the possible exit on my right.
3: I have a good feeling about light to my left
Choice: 5
Please enter a number 1 through 3.
Choice: 2
Story over!
There, that's very nice. You can have a do
expression anywhere Haskell expects an expression!marginnote(Of course, your program must typecheck. The type of a do
expression is the type of the last line.). That's one of the things that makes them so powerful. do
expressions have lots of uses besides IO, but IO is what it was originally designed for, so I thought it was fitting to introduce them that way. If you've been following along, here's the whole project up to this point.
!include(haskelltests/projects/textAdventure/src/Main.hs)
Often you'd like to send a .exe
file (or similar) to someone, so they can enjoy and experience it themselves. To do that, we need to build our project. To do that, run stack build
while in the textAdventure/
directory. In the terminal, you should see some text that says Installing executable textAdventure in <directory>
. Go to that directory, and you should see a nice little standalone file you can share with all your friends!
!quickref(Functor
is a typeclass that provides the function fmap
, which allows you to map over any Functor
. So fmap (3+) (Just 4)
evaluates to Just 7
. fmap
has a synonym, <$>
, which is useful for when you want to use it as an infix operator.
Applicative
is a typeclass that provides two functions. The first is pure, which just wraps a type in an Applicative
. So pure 3
could return Just 3
if it was used in a context where Haskell expected a value of type Maybe Int
. The second is <*>
, which applies a function inside an Applicative
to a value inside an applicative. So (Just (3+)) <*> (Just 4)
evaluates to Just 7
. All Applicative
s are also Functor
s.
Monad
is a typeclass that provides a the "bind" function, >>=
. It allows you to pass a Monad a
to a function that takes an a
and returns a Monad b
. All Monad
s are also Applicative
s.)
!newthought(In Haskell), whenever there's a container of some arbitrary "stuff" that can be mapped over, it's a good candidate for the Functor
typeclass!marginnote(Something in the Functor
typeclass is called a functor, endofunctor, or sometimes a covariant endofunctor. But you'd only ever call it anything but "functor" if you were trying to show off your category theory chops.). The most common example is Lists. There's only one function inside the Functor
typeclass, and that's fmap
, which is just like map
.
instance Functor [] where
fmap = map
In general, fmap
takes a function and a functor, and applies that function to the elements inside the functor. Let's refresh our memory of how it works on lists.
Prelude> fmap (+1) [1,2,3,4,5]
[2,3,4,5,6]
Maybe
is also a functor. This is how it works:
Prelude> fmap (+1) (Just 3)
Just 4
Prelude> fmap (+1) Nothing
Nothing
And here's how it's defined:
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
If you pass it a Just <something>
, it will apply the function to the <something>
inside. If you pass it Nothing
, it will return Nothing
. This is nice because it makes it easy for us to change the inside of a Maybe
, but won't crash on us if the result is Nothing
.
fmap
also works on 2-tuples. What they do is surprising - they only operate on the second element of the tuple. Here's a demonstration.
Prelude> fmap (+1) ("Hello", 4)
("Hello",5)
Functors have some rules, called the functor laws. There's a function called id
, which is special because it does absolutely nothing. Here's how it's defined.
id :: a -> a
id x = x
id
takes one argument, and returns that argument unchanged. The first functor law is that if you evaluate fmap id <something>
, the result should be <something>
. Whatever you pass to fmap id
should be returned unchanged.
The second functor law is that fmap (f . g) = fmap f . fmap g
. Applying fmap g
, and then applying fmap f
, should be the same as if you just applied fmap (f . g)
. This is convenient because these two laws mean that fmap
probably won't be doing anything other than just mapping over your function. Note that the functor laws aren't enforced by Haskell - it's up to the programmer to ensure they're true. If you make a data type an instance of Functor
, you should make absolutely sure that it doesn't break any of the laws.
!newthought(A monoid) is very simple. It's a function (or operator) f which takes two arguments!marginnote(A function (or operator) which takes two parameters is also called a binary function.), and has a value that can be used as an identity, and is associative. We have two new words here, associative and identity. Let's discuss what they mean with three famous monoids, addition (+
), multiplication (*
), and string concatenation (<>
).
Let's look at addition.
-
Does it take two arguments? Yes.
Prelude> 2 + 5 7
-
Is it associative? That means parentheses don't matter when evaluating it!(To rearrange the parentheses in an expression is to reassociate it. The technical definition of "associative" is that you can reassociate any way you want without changing the meaning of the expression. ).
Prelude> 2 + 5 + 7 + 8 22 Prelude> (2 + 5) + (7 + 8) 22 Prelude> (2 + (5 + 7 + 8)) 22
Seems that way!
-
Does it have an identity? For addition, that means "is there a value you can add to any number, and leave that number unchanged". There is,
0
.Prelude> 16 + 0 16
Let's look at multiplication.
-
Does it take two arguments? Yes.
Prelude> 2 * 5 10
-
Is it associative? That means parentheses don't matter when evaluating it.
Prelude> 2 * 5 * 7 * 8 560 Prelude> (2 * 5) * (7 * 8) 560 Prelude> (2 * (5 * 7 * 8)) 560
Seems that way!
-
Does it have an identity? For multiplication, that means "is there a value you can multiply by any number, and leave that number unchanged". There is,
1
.Prelude> 16 * 1 16
Now, let's look at string concatenation.
-
Does it take two arguments? Yes.
Prelude> "Hello, " <> "World!" "Hello, World!"
-
Is it associative? That means parentheses don't matter when evaluating it.
Prelude> "Good morning, " <> "good afternoon, " <> "good evening, " <> "and good night!" "Good morning, good afternoon, good evening, and good night!" Prelude> ("Good morning, " <> "good afternoon, ") <> ("good evening, " <> "and good night!") "Good morning, good afternoon, good evening, and good night!" Prelude> ("Good morning, " <> ("good afternoon, " <> "good evening, " <> "and good night!")) "Good morning, good afternoon, good evening, and good night!"
Seems that way!
-
Does it have an identity? For string concatenation that means "is there a value you can concatenate to any string, and leave that string unchanged. There is,
""
.Prelude> "Monoids!" <> "" "Monoids!"
Hopefully, this should give you an intuition for monoids. Since we're in a chapter about typeclasses, you might be confused. Being a monoid seems to be a property about operators, what does this have to do with types? Well, I mislead you somewhat. The correct phrasing would be to say that String
s form a monoid under <>
. It's the type that's the monoid, not the operator. Of course, one type can be a monoid under multiple operations, but in Haskell, we usually single out one operation to be "the" operation for that monoid. For example, String
s have <>
, so String
is a monoid.
Here's how the Monoid
typeclass is defined:
class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty
Let's look at what each of these means.
mempty
is a function that returns the identity value for whatever monoid is in question. It's short for "monoid empty".
Prelude> mempty :: String
""
Prelude> mempty <> "hello"
"hello"
There's mappend
, which takes two Monoid
s and does whatever our monoid operation is on them. Instead of mappend
, you can also write <>
!marginnote(Yes, the very same <>
you've been using for combining lists will work for any Monoid!).
Prelude> "a" `mappend` "b"
"ab"
-- <> and `mappend` are identical here.
Prelude> "a" <> "b"
"ab"
Lastly, there's mconcat
, which takes a List of monoids and mappend
s them all.
Prelude> mconcat ["1", "2", "3", "4"]
"1234"
You may have noticed there are two operations under which numbers are monoids, +
and *
. Because of this, most number types aren't in the Monoid
typeclass, since they couldn't pick one operation to be the operation for numbers. It does, however, have the types Product
and Sum
in the Data.Monoid
module.
Prelude> import Data.Monoid
Prelude> Product 3
Product {getProduct = 3}
Prelude> (Product 3) `mappend` mempty
Product {getProduct = 3}
You may be wondering, what exactly is this Product {getProduct = 3}
mess? It's because Product
is a type which acts as a wrapper around a number, so it can be in different typeclasses. getProduct
is the function we can use to get the actual number.
Prelude> threeProduct = Product 3
Prelude> threeProduct
Product {getProduct = 3}
Prelude> getProduct threeProduct
3
Of course, there's also Sum, which works the same way except its monoidal operation is +
instead of *
, and you extract the original number with getSum
.
Prelude> a = Sum 3
Prelude> b = Sum 5
Prelude> a `mappend` b
Sum {getSum = 8}
Prelude> getSum a
3
2-tuples can also be monoids, provided they contain monoids. The contents on the inside are just mappend
ed to each other.
Prelude> ("Hello ", "Goodbye ") `mappend` ("world.", "planet.")
("Hello world.","Goodbye planet.")
Let's see how that works with the Sum
an Product
types.
Prelude> import Data.Monoid
Prelude> (Sum 3, Product 4) `mappend` (Sum 10, Product 2)
(Sum {getSum = 13},Product {getProduct = 8})
So if you have two tuples of type (a, b)
, and both a
and b
are Monoid
s, then you can mappend
the tuples and the contents will be mappend
ed. Since regular numbers aren't monoids, this won't work.
Prelude> (1,2) `mappend` (2,3)
-- error!
!newthought(Remember, if) you make one of your own types an instance of Monoid
, it's important that you make sure that your implementation follows the monoid laws of associativity and identity. If not, other programmers and yourself can get very confused!
!newthought(*Semigroups are) very simple - they're just monoids, but without the restriction that we must have an identity element. This means all monoids are semigroups. Any associative binary function forms a semigroup. Let's see an example with the max
function. The max
function takes two values and returns whichever is greater.
Prelude> max 3 5
5
Prelude> 3 `max` 5
5
Is it associative? Let's try.
Prelude> 1 `max` 2 `max` 54 `max` 19 `max` (-1)
54
Prelude> 1 `max` (2 `max` (54 `max` 19) `max` (-1))
54
Prelude> (1 `max` 2 `max`) (54 `max` 19) `max` (-1)
54
Yes! A chain of max
s will eventually output whichever value is highest, no matter where we put the parentheses. This means max
is associative, so numbers form a semigroup under max
. We're able to unravel the mystery of <>
- it's the associative binary function that anything in the typeclass Semigroup
has. If a type is an instance of Monoid
and Semigroup
, you can expect <>
to be the same as mappend
.
Prelude> "Hello," <> " World."
"Hello, World."
Semigroup isn't used very often, so let's move on.
!newthought(An applicative) is essentially a monoid mixed with a functor. Let me explain what that means.
As we've discussed before, Haskell functions are curried. This means that, while you can write 1+1
just fine, you can also just write 1+
, and then you get a function which will add one to whatever number you pass it. Let's play with that.
Prelude> :t fmap (+) (Just 3)
fmap (+) (Just 3) :: Num a => Maybe (a -> a)
Prelude> :t fmap (+) [1,2,3,4,5,6]
fmap (+) [1,2,3,4,5,6] :: Num a => [a -> a]
fmap (+) (Just 3)
returns a Num a => Maybe (a -> a)
, a Maybe
that contains a function. fmap (+) [1,2,3,4,5,6]
returns a List of functions.
But we have an issue. If we have a Just (1+)
, it'd be nice to have an easy way to apply that to the inside of a Just 4
to get Just 5
. That's where Applicative
comes in.
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
You can see that for f
to be an Applicative
, it must first be a Functor
. That's because Applicative
s are just functors with extra features. It contains the pure
function, which takes a value and wraps it up inside the applicative.
Prelude> pure 4 :: Maybe Int
Just 4
Prelude> pure 4 :: [Int]
[4]
The intelligence of Haskell's type analysis allows it to select the correct pure
function to call, based on the types it thinks we want. Here we use ::
to manually tell Haskell the type we want, just like we sometimes have to do for read
. pure
takes a value of any type and wraps it up inside an Applicative
. If it seems familiar, it should! It's the same function we used to wrap values up into IO values earlier!
The other function all Applicative
s have is <*>
. Compare its type to the type of fmap
:
fmap :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
Can you guess what <*>
does? It's just like fmap
, but where fmap
wanted a function and a value wrapped up in a functor, <*>
wants a function and a value both wrapped up in an applicative. That's a mouthful, let's see it in action.
Prelude> (+1) `fmap` (Just 10)
Just 11
Prelude> (Just (+1)) <*> (Just 10)
Just 11
You could even chain it up even more.
Prelude> (Just (+)) <*> (Just 1) <*> (Just 10)
Just 11
Prelude> Nothing <*> (Just (+))
Nothing
Here's how Maybe
is made to be an instance of Applicative
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> x = fmap f x
Unfortunately, we're unable to mix and match Applicative
s like we might wish we could.
Prelude> (Just (+)) <*> [1,2,3,4]
-- error!
But since both sides have to be the same type of Applicative
, this means Haskell can figure out what types we want from just one argument.
Prelude> (Just (+5)) <*> (pure 5)
Just 10
And it can also be chained, like this!marginnote(Recall the take
function, for which take 3 [1..]
evaluates to [1,2,3]
. It take a list and truncates it.)!
Prelude> (Just take) <*> (Just 3) <*> (Just [1,2,3,4])
Just [1,2,3]
There's also <$>
, equivalent to fmap
but an operator, which allows us to use it in infix notation.
Prelude> fmap (+1) [1,2,3]
[2,3,4]
Prelude> (+1) <$> [1,2,3]
[2,3,4]
It's really useful because it combines well with <*>
.
Prelude> (<>) "Haskell" "!"
"Haskell!"
Prelude> (<>) <$> Just "Haskell" <*> (pure "!")
Just "Haskell!"
Prelude> take 5 [1,2,3,4,5,6,7,8,9,10]
[1,2,3,4,5]
Prelude> take <$> Just 5 <*> Just [1,2,3,4,5,6,7,8,9,10]
Just [1,2,3,4,5]
Notice how we use <$>
first, to apply fmap
from (<>)
to Just "Haskell"
to get Just ("Haskell"<>)
. We then use <*>
on Just "!"
to apply ("Haskell"<>)
to "!"
. So if you want to use a normal function on values wrapped up in Applicative
s (also called applicative functors), you can just use <$>
and <*>
as required. Don't worry if this seems confusing, try looking at the second example (with take
) and try and work through it.
Exercises:
-
Rewrite
take <$> Just 5 <*> Just [1,2,3,4,5,6,7,8,9,10]`
so it uses
fmap
instead of<$>
-
Use
<$>
and<*>
to applyrem
toJust 16
andJust 4
. The result should beJust 0
.
!newthought(To see) how applicatives work on lists, let's use <*>
and <$>
to generate some usernames.
Prelude> superlatives = ["Smelliest", "Steamiest", "Spiciest"]
Prelude> nouns = ["Malt", "Mold", "Meat"]
Prelude> (<>) <$> superlatives <*> nouns
["SmelliestMalt","SmelliestMold","SmelliestMeat","SteamiestMalt","SteamiestMold","SteamiestMeat","SpiciestMalt","SpiciestMold","SpiciestMeat"]
What's happened here is <*>
has applied every function in the List in the left-hand argument to every value in the List of the right-hand argument. We make these functions from (<>) <$> superlatives
, which turns all our superlatives into functions that prepend that superlative to another string. The result: A List of usernames that you're free to use next time you need to create an account for something.
Here's what makes applicatives monoidal functors. Notice this similarity:
($) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
Remember, $
is function application. So f $ x
is equivalent to f x
. It takes a function and a value and applies that function to that value.
<$>
is fmap
. To use Maybe
as an example, f <$> (Just x)
is the same as Just (f x)
. It take s a function and a functor, and applies that function to the contents of the functor,
<*>
is fmap
, only it works on applicatives and the function you pass it is also wrapped up. So (Just f) <*> (Just x)
is equivalent to Just (f x)
.
So, what exactly does Applicative
have to do with Monoid
? The technical term is it's a monoidal functor. The trick is, you see those f
s that wrap up the values in <*>
?
(<*>) :: f (a -> b) -> f a -> f b
Look at the Maybe
type in particular.
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
-- 1st argument 2nd argument output
Maybe
can be Just <something>
or Nothing
. If the first argument is Nothing
, or the second argument is Nothing
, the output is Nothing
. That can be thought of as an associative binary function! This is kind of vague for Maybe
but for some types like (String, a)
it's much more clear.
Prelude> ("hello ", (4+)) <*> ("world", 10)
("hello world",14)
What's going on here is <*>
is getting two 2tuples. The first one is of type (String, Int -> Int)
and the second one is of type (String, Int)
. It combines the two String
s using mappend
!marginnote(mappend
for String
s is just <>
.) and it applies the function in the first tuple to the value in the second tuple. This could be used for logging, for example:
Prelude> ("Multiplying by 10. ", (10*)) <*> (("Adding 4. ", (4+)) <*> ("Original value 10. ", 10))
("Multiplying by 10. Adding 4. Original value 10. ",140)
The String
s are combined with mappend
, and the values on the right are mapped. This makes any 2-tuple with a Monoid
in the first position an Applicative
.
!newthought(Monads have) a reputation of being difficult to learn. James Iry was making a joke when he said "A monad is just a monoid in the category of endofunctors, what's the problem?", but he wasn't totally right!sidenote(That'd be a better description for an applicative, although you can argue it applies to monads).
Monads are pretty simple. Just like you can imagine functors being applicatives with extra features, you can imagine a monad being an applicative with extra features. Indeed,Everything in the Monad
typeclass is also in the Applicative
typeclass. Since everything in the Applicative
typeclass is also in the Functor
typeclass, that means that all monads are applicatives and functors! But they're far more powerful than either applicatives or functors, which is why you see them everywhere in Haskell, from lists to IO
and even to the humble Maybe
.
Here's a reminder for all the terms we've introduced in this chapter.
-
A functor is something that can be mapped over (using
fmap
or<$>
in Haskell). Examples include Lists andMaybe
.fmap
takes a function and a functor, and applies the function to the contents of the functor. Sofmap (+1) (Just 3)
evaluates toJust 4
. A functor must -
An applicative is a functor, so it still has
fmap
, but it also has two additional operations!marginnote(Most functors are applicatives these days.).-
It has
pure
, which takes a value and wraps it up in an applicative. Sopure 3 :: [Int]
evaluates to[3]
, andpure 3 :: Maybe Int
-
It has
<*>
, which is just likefmap
except both the function being passed and the value to apply it to are both wrapped up in applicatives. SoJust <*> (Just 3)
evaluates toJust 4
(Compare that to(+1) <$> (Just 3)
, which also evaluates toJust 4
).
-
To understand monads, many people find the use of analogies helpful. Monads usually provide context to a value, sometimes called computational context. They're used like modifiers to other types. For example, you may have a String
. If this String
represents someone's licence plate, it might not always exist, such as when that person does not have a car. So you might choose to represent this with a Maybe String
, so my licence plate would be Just "ABCD123"
and my friends who take the subway would be Nothing
. Maybe
has given us some context for the String
, we've said it may not exist. But you may have noticed an issue here - what if someone has multiple cars! That would call for a different context, the context of zero or more. To represent that, we'd use a [String]
. These are the monads we're going to be spending most of our time discussing - Maybe
and []
.
That should give you a little bit of an intuition for monads, so it's time to get specific. To start us off, let's look at the Monad
typeclass definition.
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
These functions should look pretty familiar. return
is just pure
, which you should recognize from Applicative
s!marginnote(return
is a relic of the time Haskell developers didn't know about applicatives, it only still exists for backwards compatibility reasons. pure
will work with any Applicative
while return
will only work with Monad
s, so pure
is strictly better.). It takes a value and wraps it up in an Applicative
(or in this case, not just any Applicative
, but a Monad
).
The other function monads provide us is >>=
the bind operator. There's also >>
, but it's just a simpler version of >>=
so we'll skip over it for now.
Look at the type signature for >>=
:
(>>=) :: m a -> (a -> m b) -> m b
It's actually pretty simple. The goal of >>=
is to take a value m a
, and a function from a
to m b
, and apply that function to the value. How would we do that for Maybe
? Here's how!
Nothing >>= f = Nothing
Just x >>= f = f x
Easy as pie! That's all we need for the Maybe
monad. Let's put this to the test. Do you remember our function maybeHead
?
-- maybeHead.hs
!include(haskelltests/should_compile/maybeHead.hs)
Here's how it works, in case you forgot.
Prelude> maybeHead [1,2,3]
Just 1
Prelude> maybeHead []
Nothing
This takes a [a]
and returns a Maybe a
. But what if we wanted to pas it a Maybe [a]
? We do this with >>=
!marginnote(Compare a -> m b
to [a] -> Maybe a
. maybeHead
takes a parameter of a certain type ([a]
), and returns a parameter of a different type (a
) wrapped in a Maybe
.).
Prelude> xs = Just [1,2,3]
Prelude> xs >>= maybeHead
Just 1
>>=
unwrapped the [a]
out of the Maybe [a]
and passed it to maybeHead
, which returned Just 1
. But if our Maybe [a]
was Nothing
, this is what would have happened:
Prelude> xs = Nothing
Prelude> xs >>= maybeHead
Nothing
If the left hand of >>=
is Nothing
, that's what the output will be. For proof of this, we can try it with undefined
, a special value which we can pass to any function but that function isn't allowed to look at or use:
Prelude> Nothing >>= undefined
Nothing
When you think about it, this is exactly what we want! Maybe
means there's a chance our value doesn't exist. Once it's Nothing
, we don't want to do anything with it anymore, we just want everything after it to be Nothing
.
How can we use this? Remember our maybeDivide
function?
-- maybeDivide.hs
!include(haskelltests/should_compile/maybeDivide.hs)
Here's how it works, in case you've forgotten. It divides two numbers, and returns Just <result>
. However, if the denominator is 0
, it will return Nothing
.
Prelude> maybeDivide 2 4
Just 0.5
Prelude> maybeDivide 2 0
Nothing
Can we use this with >>=
? Let's see!
Prelude> xs = Just [2,3,4]
Prelude> xs >>= maybeHead >>= (maybeDivide 3)
Just 1.5
This won't crash, even if you have things that would normally cause issues, such as empty lists or division by 0
!
Prelude> Nothing >>= maybeHead >>= (maybeDivide 3)
Nothing
Prelude> (Just []) >>= maybeHead >>= (maybeDivide 3)
Nothing
Prelude> (Just [0]) >>= maybeHead >>= (maybeDivide 3)
Nothing
If one of these functions outputs Nothing
at any stage, the output will be Nothing
. This is quite an elegant way to handle errors because we only have to check once, at the end. You can see how >>=
makes working with Maybe
much more convenient.
Let's try some silly examples using lambdas!marginnote(A lambda is a function you can write in an expression, without giving it a name. Their format is \parameter1 parameter2 -> <expression>
. They're very useful when working with functions like >>=
, which expects a function of type a -> m b
for its right-hand argument.).
Prelude> (Just 0) >>= (\x -> Just (x + 1)) >>= (\x -> Just (x * 3)) >>= (\x -> Just (show x)) >>= (\x -> Just ("And the answer is " <> x))
Just "And the answer is 3"
Each of these lambdas we've made takes a plain value and returns a Maybe
value. >>=
does all the work of extracting the value out of the Maybe a
and passing it to our lambda. Here's how we might do it if we were writing a program. Remember though that this example is silly, and is only to show off the fact that >>=
is unwrapping our values for us.
-- sillyMaybeBindTest.hs
!include(haskelltests/should_compile/sillyMaybeBindTest.hs)
If we really wanted to do all this though, we should do it with something called do
expressions. All do
notation does is take away the hard work of typing out all our lambdas by hand.
-- sillyMaybeDoTest.hs
!include(haskelltests/should_compile/sillyMaybeDoTest.hs)
Notice that we don't use an x <-
for the last line!marginnote(You may notice that this looks like something you might expect from imperative programming languages. It's this similarity that's caused some to say "Haskell is the world’s finest imperative programming language" (That, in addition to Haskell's impressive ability to abstract over imperative ideas, such as while
and for
loops). That said, it's generally best to use an imperative programming language if you intend to mostly be using the imperative programming style.). That's because, like >>=
, the result of do
is the result of the last line. It doesn't make sense to give a name to the output of the last line. You don't need to have an x <-
for any of the lines, although you won't be able to reference previous values if you don't.
If we wanted to, we could use pure
instead of Just
. As long as we use Just
at least once, Haskell will know we want the output of our do
expression to be a Maybe
value, so it will call the version of pure
that returns a Maybe
.
-- sillyMaybeDoTestPure.hs
!include(haskelltests/should_compile/sillyMaybeDoTestPure.hs)
Of course, the result of all these is the same:
*Main> result
Just "And the answer is 3"
By the way, reusing previous names in do
expressions is called shadowing and it is generally considered bad practice. If you can't think of good names, something like this is always an option!marginnote(My preferred way to type subscripts like ₁
is to use the Latex Input extension in VSCode, which allows me to simply type \_1
):
-- sillyMaybeDoTestSub.hs
!include(haskelltests/should_compile/sillyMaybeDoTestSub.hs)
Note that if any of these evaluate to Nothing
, the whole thing will evaluate to Nothing
.
-- sillyMaybeDoTestNothing.hs
!include(haskelltests/should_compile/sillyMaybeDoTestNothing.hs)
*Main> result
Nothing
You may think this is strange, since y <- Nothing
seems like it shouldn't influence the result at all. But consider what would happen if you then tried to use y
. When you write x <- Just 3
, x
becomes 3
(Not Just 3
). What would y
be?
!newthought(Like functors) and monoids, monads have laws that your Monad
instances should obey.
The first law is that pure x >>= f
should be equivalent to f x
. Remember, pure
just wraps a value up in some other type, like Maybe
. Then >>=
unwraps it and passes it to f
. Together, they undo each other.
The second law is that m >>= pure
should be equivalent to m
. This is because >>=
is responsible for unwrapping m
and passing it to the function on the right (in this case pure
). Then pure
wraps it right back up. This means they should undo each other the other way, too.
The third law is that (m >>= f) >>= g
should be equivalent to m >>= (\x -> f x >>= g)
. What it's basically saying is that monads should be associative - the parentheses shouldn't really matter!marginnote(This might remind you of monoids, and that's because monads and monoids are closely related in category theory. If you're interested in how exactly, read Category Theory for Programmers.).
!newthought(In the) last chapter, we discussed the Maybe
monad. But there are other monads as well, such as []
. Here's its definition:
instance Monad [] where
xs >>= f = concat (fmap f xs)
So, you pass >>=
a list [a]
and a function a -> [b]
. It applies the function to every item in the list (creating a list of lists), then concatenates all the lists together. Here's a demonstration:
*Main> [1,2,3] >>= \x -> [x, x*1000]
[1,1000,2,2000,3,3000]
The 1
in [1,2,3]
is turned into [1, 1000]
. The 2
is turned into [2, 2000]
and the 3
is turned into [3,3000]
. All these lists are concatenated together to get [1,1000,2,2000,3,3000]
.
A natural way to think about this is functions that can return multiple values. Imagine a sqrt
function. As you might be aware, sqrt
has two values that would make sense to return - a positive and a negative. There's a function in Haskell called sqrt
that squares a number and returns the positive, let's make sqrt'
which does it right.
sqrt' x = let root = sqrt x
in [root, -root]
Now, let's test it out!
Prelude> sqrt' 4
[2.0,-2.0]
Now, imagine we wanted to find every 4th root. We can just apply sqrt'
twice!
Prelude Data.Complex> sqrt' 4 >>= sqrt'
[1.4142135623730951,-1.4142135623730951,NaN,NaN]
Well... that's disappointing. When you run sqrt
on a negative number, it returns NaN
!marginnote(If you run sqrt
on a complex number it will return the correct result, not Nan
(complex numbers are available in Data.Complex
and made using the :+
operator).).
Hopefully this communicates what the list monad is doing. Monads aren't anything super complicated or hard - they're just an additional context for a value.
!quickref(If a project was created with stack new
, you can add 3rd party dependencies in the package.yaml
file under the dependencies
section, and they will be automatically downloaded the next time you build.
Testing is done by writing tests in the test
folder, typically by using hspec
and quickcheck
. Tests can be run using stack build --test
Documentation is written in the form of haddock comments, which have the following form:
-- | Documentation line 1
-- Documentation line 2
-- ...
f :: String -- ^ Description of what this argument represents.
-> String -- ^ Description of what this argument represents.
-> Bool -- ^ Description of the output of the function
f s₁ s₂ = ...
Haddock documentation can be built using stack build --fast --haddock
or stack build --fast --haddock-deps
if you only care about haddock for your dependencies.
Haddock documentation can be viewed through hoogle. To use hoogle, first build a hoogle index with stack hoogle -- generate --local
, then run the server with stack hoogle -- server --local --port=8080
. You can then search and view the functions in your code with by visiting localhost:8080
in a web browser.)
!newthought(This chapter) will tell you the last few things you still need to know to become a genuine Haskell dev. After this, you'll have the skills you need to start being really productive! A project we'll be making soon is going to be called irc
, so go ahead and run stack new irc
somewhere to generate it (notice we don't use stack new irc simple
- we'll be using some features not in the simple
template). We'll actually be making a little IRC client! If you're not familiar, IRC is an internet protocol for making online chat rooms. Anyone can host an IRC server, which can have multiple chat rooms inside it called channels.
!newthought(You may) remember the tool we used earlier, Stack. Stack isn't a package manager, it's responsible for building your code, which occasionally involves downloading packages from the internet. It doesn't try to manage the packages your program uses, it just builds what are called "targets" (and their dependencies). To build a target, you want to use stack build <target>
. You very rarely want to use stack install
. stack install
is for installing Haskell programs to your computer, it's totally different from npm install
or pip install
. You probably don't want to use stack install
(It almost got removed entirely to eliminate this exact confusion).
To add a dependency, you write its name in the package.yaml
file under the dependencies
section. Let's add one called lens
(Lens is a library that has many useful things related to getting and setting values). We'll also add one called hspec
and QuickCheck
for testing our project, and one called network
which we'll use to communicate to IRC servers.
Go to our irc
folder and look at package.yaml
. Change the dependencies:
line (line 22) from
dependencies:
- base >= 4.7 && < 5
To
dependencies:
- base >= 4.7 && < 5
- hspec
- QuickCheck
- lens
- network
This change tells stack
that our code depends on a library called hspec
, a library called lens
, and a library called network
. base
is the basic stuff all Haskell programs use, like numbers (the >=4.7 && < 5
tells cabal which versions of base
it can expect our code to work for).
To test it out, run stack ghci
while in the irc
folder. This will build your project and open a new GHCi instance from which you can import libraries you've added to your project.
By default, our program just prints the text somefunc
. Let's test that out by typing main
(remember, main
is the function run by Haskell when you turn it into an .exe).
*Main Lib> main
someFunc
Also, we have access to the libraries we just imported. To test that out, import the Lens library with import Control.Lens
. It's useful for getting values from tuples.
*Main Lib> import Control.Lens
*Main Lib Control.Lens> view _1 ("hello", "goodbye", "how")
"hello"
You'll see that confusingly, Lens indices for tuples start at 1.
Let's briefly discuss the structure of this project. The main
function for our project is in app/Main.hs
. It imports the Lib
module which we define in src/Lib.hs
. Inside Lib.hs
we define a function called someFunc
, so we're able to call it inside Main.hs
.
-- app/Main.hs
module Main where
import Lib
main :: IO ()
main = someFunc
-- src/Lib.hs
module Lib
( someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
The first couple lines in src/Lib.hs
are new to us. They're written like:
module Lib
( someFunc
) where
But that's just for ease of input, they could have also written it like this:
module Lib (someFunc) where
What this means is that Lib
only exports the someFunc
function (when you import a module, you only get what it exported). By default modules export everything, adding (someFunc)
tells it to only export someFunc
. Only exporting what you have to is considered good practice in Haskell.
The idea is that all of our little helper functions go in Lib
, and then our actual code to run our app goes in Main
. This is because any code in the src
folder can be automatically tested, but code in the app
folder cannot. Next, we'll write our automatic tests in Spec.hs
.
You may be familiar with the Stack command stack build
. It's what we used to build our textAdventure
project into an executable we could distribute to our friends and family. We could run this executable to test our project manually, but that's a bit repetitive. A more modern Haskell-ey way is to write our tests in code, then have those run. The library we do that with is one called hspec
, which we set up in the last chapter.
To run our tests, we just run stack build --test
(or just stack test
for short). This will build our code and then run the tests. stack build
can be pretty slow because GHC spends a lot of time optimizing, so you can use stack build --fast --test
instead. But you shouldn't use fast
to make a production build since it trades fast builds for slow code. If you run this, you should see:
Test suite not yet implemented
That's because we haven't added any tests yet! Let's write our first function and then test it. As a reminder, we're making a program that'll let us talk in IRC chatrooms. We'll let the user enter a channel (the IRC term for a chatroom) which our program will join and talk in. Channels in IRC must begin with a #
and can't contain a space. Let's start writing a function that will take a String
and return a Bool
telling us whether it's a valid name for a channel. We'll do this in src/Lib.hs
, so we can test it.
-- src/Lib.hs
module Lib
( someFunc,
isValidChannelName -- We also want to export isValidChannelName, so we add it here.
) where
-- We'll just check if the first letter is '#'.
isValidChannelName :: String -> Bool
isValidChannelName s = (head s) == '#'
someFunc :: IO ()
someFunc = putStrLn "someFunc"
Now, let's write a test for this! Go into test/Spec.hs
.
-- test/Spec.hs
main :: IO ()
main = putStrLn "Test suite not yet implemented"
Not much to it. Add import Test.Hspec
and import Lib
to the top.
-- test/Spec.hs
import Test.Hspec
import Lib
main :: IO ()
main = putStrLn "Test suite not yet implemented"
This will give us access to the hspec
functions, which we'll use for testing the functions in Lib
. Then, we'll add a test. Tests are easy to understand once you've seen them.
-- test/Spec.hs
import Test.Hspec
import Lib
main :: IO ()
main = hspec $ do
describe "Lib.isValidChannelName" $ do
it "allows channels with # in the first character" $ do
isValidChannelName "#haskell" `shouldBe` (True :: Bool)
isValidChannelName "#ghc" `shouldBe` (True :: Bool)
hspec
uses a lot of nested do
s. Let's add another test to make sure we don't just accept anything.
-- test/Spec.hs
-- [...]
main :: IO ()
main = hspec $ do
describe "Lib.isValidChannelName" $ do
it "allows a channel with # in the first character" $ do
isValidChannelName "#haskell" `shouldBe` (True :: Bool)
isValidChannelName "#ghc" `shouldBe` (True :: Bool)
it "disallows channels without # in the first character" $ do
isValidChannelName "haskell" `shouldBe` (False :: Bool)
Now, run stack build --fast --test
and see what it says! Both of our tests will pass. This kind of testing is known as spec testing, and is very useful. But it has some flaws: we have to write every test ourselves, which is time-consuming. Also, we might miss some edge case we didn't think about! That's where property testing comes in, with QuickCheck
(which comes with Hspec
). Import Test.Quickcheck
at the top.
-- test/Spec.hs
import Test.Hspec
import Test.QuickCheck
import Lib
-- [...]
The idea behind property testing is instead of giving specific tests, you give a function that does a test based off a random input. Here's how it works:
-- test/Spec.hs
-- ...
main :: IO ()
main = hspec $ do
describe "Lib.isValidChannelName" $ do
it "allows a channel with # in the first character" $ do
isValidChannelName "#haskell" `shouldBe` (True :: Bool)
isValidChannelName "#ghc" `shouldBe` (True :: Bool)
it "disallows channels without # in the first character" $ do
isValidChannelName "haskell" `shouldBe` (False :: Bool)
it "allows channels should contain a '#'" $ do
property $ \xs -> if isValidChannelName xs
then elem '#' (xs :: String)
else True
instead of using shouldBe
, we've made a property. The property is a function that should always return True if your function works correctly. Now we test that if our function says something is a valid channel name, that channel name should contain a #
. QuickCheck will generate a load of random values!marginnote(By default, QuickCheck does around 100 tests.) and pass them to your function, and if your function ever returns False
it'll be counted as a test failure. Let's run our tests with stack build --fast --test
. Lo and behold, we find a bug!
Failures:
test\Spec.hs:14:7:
1) Lib.isValidChannelName Allowed channels should contain a '#'
uncaught exception: ErrorCall
Prelude.head: empty list
(after 1 test)
""
It says our function threw an uncaught exception, specifically, it throws an exception when we pass it an empty list! Let's fix our function.
-- src/Lib.hs
isValidChannelName :: String -> Bool
-- Use pattern matching to return False when passed an empty list.
isValidChannelName [] = False
isValidChannelName s = (head s) == '#'
Now when running stack build --fast --test
, we get 3 examples, 0 failures
! Property tests with QuickCheck can be a powerful tool to validate that your programs run properly, in addition to spec tests with Hspec. Any time you fix a bug in one of your functions, you should write a spec or property test to make sure it doesn't pop up again. This is called regression testing, and it often comes easy, since if you think a function has a bug the first thing you should do is write some tests for it.
You can also see how much of your code you've tested by telling stack to run a code coverage report. You can do this with stack build --fast --test --coverage
. This should generate something like:
irc-0.1.0.0: Test suite irc-test passed
Generating coverage report for irc's test-suite "irc-test"
71% expressions used (5/7)
100% boolean coverage (0/0)
100% guards (0/0)
100% 'if' conditions (0/0)
100% qualifiers (0/0)
100% alternatives used (2/2)
100% local declarations used (0/0)
50% top-level declarations used (1/2)
The most important one is the last one - 50% top-level declarations used
. This essentially tests how many top-level functions (functions not inside another function) you've got at least one test for. Our Lib
module has two, someFunc
and isValidChannelName
, and we've only tested the latter so that leaves us with 50% top-level declarations. We'll be getting rid of someFunc
soon so we won't bother testing it.
Also, if you don't feel like writing a test, you can just write pending
, like this.
main :: IO ()
main = hspec $ do
describe "Lib.isValidChannelName" $ do
it "allows a channel with # in the first character" $ do
isValidChannelName "#haskell" `shouldBe` (True :: Bool)
isValidChannelName "#ghc" `shouldBe` (True :: Bool)
it "disallows channels without # in the first character" $
isValidChannelName "haskell" `shouldBe` (False :: Bool)
it "allows channels should contain a '#'" $ do
property $ \xs -> if isValidChannelName xs
then elem '#' (xs :: String)
else True
it "disallows spaces" $ do
pending
This is a helpful reminder that you still need to write that test (Stack will remind you when you run your tests). If there's some reason you haven't written the test besides laziness, you can use pendingWith
.
it "disallows spaces" $ do
pendingWith "This is an exercise for you, the reader! \
\Write a spec test to test if our function \
\allows a channel name with a space."
Now, replace this pendingWith
with a spec test to see that our function won't accept a channel with a space like "#haskell programming"
. We haven't implemented checking for spaces yet, so this test should fail until we do. It's always a good idea to write a test for features you haven't implemented yet, so you won't forget. Also, you may have noticed the backslashes inside this string. That's how you spread strings across multiple lines, which can be used to make your code easier to read.
Exercises:
-
Write a spec test that ensures that
isValidChannelName
returnsFalse
when passed an empty string. -
Write a spec test that tests that
isValidChannelName
returnsFalse
when passed"#learn Haskell"
. This test should fail. -
Write a property test that takes a value
xs
and checks thatisValidChannelName (xs <> " ")
isFalse
. -
In
Lib.hs
'sisValidChannelName
, usenot (elem ' ' s)
to check that the inputString
does not contain a space. You can do this with the binary&&
operator,if/then/else
syntax, or Guards. -
After completing #4, rerun your tests. They should pass!
Often, we would like to explain what a certain function does, in a way that's not easily represented in the function's name. For example, maybe we would like to say that our isValidChannelName
function will return True
in cases where the first character is #
and the String
does not contain a space. This is called documentation. We can do this with a comment, but there's a special way to annotate our comments so they can be made into a pretty website, using a tool called Haddock.
Here's our isValidChannelName
function, with a descriptive comment:
-- |'isValidChannelName' takes a 'String' and will return 'True' if the 'String' begins with a '#' and does not contain a space.
isValidChannelName :: String -> Bool
isValidChannelName [] = False
isValidChannelName s
| (head s) /= '#' = False
| (elem ' ' s) = False
| otherwise = True
The -- |
is special syntax that informs GHC that this is Haddock documentation. It must be placed before the type signature. We can also spread it out across multiple lines, like this:
-- |'isValidChannelName' takes a 'String' and will return 'True'
-- if the 'String' begins with a '#' and does not contain a space.
isValidChannelName :: String -> Bool
isValidChannelName [] = False
isValidChannelName s
| (head s) /= '#' = False
| (elem ' ' s) = False
| otherwise = True
We can also use -- ^
to give details about individual arguments and the return value:
-- |'isValidChannelName' takes a 'String' and will return 'True'
-- if the 'String' begins with a '#' and does not contain a space.
isValidChannelName :: String -- ^ The channel name.
-> Bool -- ^ 'True' if the channel name is valid, 'False' otherwise.
isValidChannelName [] = False
isValidChannelName s
| (head s) /= '#' = False
| (elem ' ' s) = False
| otherwise = True
There's a lot of special markup you can use to make your documentation prettier, you can read about it in the Haddock documentation.
Now, you can build your haddock documentation with stack using the --haddock
flag. To build and test your code without optimization, plus build haddock, run stack build --test --fast --haddock
. Now, Haddock often takes a long time to run, and you usually don't really care that much about seeing your own haddock documentation, but instead you're more interested in seeing the documentation of your dependencies (our current dependencies are hspec
, QuickCheck
, lens
, and network
). After the first time it's much faster to only build those, and we can do that with haddock-deps
, since it won't run at all if we haven't added new dependencies. This is our new command:
stack build --test --fast --haddock-deps
Now, we can read the documentation for any of our dependencies with stack haddock --open <something>
. For example, we can look at the documentation for lens
with stack haddock --open hspec
. This is better than reading the docs online because you're guaranteed to be reading the docs for the right version of whatever library you're using.
You can also get a searchable version of these docs with Hoogle. You must first run a command to generate Hoogle's index:
stack hoogle -- generate --local
This will probably take some time, especially since it'll probably have to download hoogle. You'll have to rerun this whenever you add any more dependencies but it should be much faster after the first time. After this you just need to start a Hoogle server with one easy command:
stack hoogle -- server --local --port=8080
Then go to http://localhost:8080 and try doing a search! Maybe look at the type of the property
function we used for writing property tests in QuickCheck. By the way, if you ever add a new dependency, you need to re-run stack hoogle -- generate --local
.
GHC had the concept of "warnings" in addition to errors. This is when GHC sees things that it thinks might be the source of bugs in the future. Most warnings are turned off by default, but you can put something in your package.yaml
to turn them on. Open your package.yaml
find the dependencies section:
dependencies:
- base >= 4.7 && < 5
- hspec
- QuickCheck
- lens
- network
We're gonna add something right below it:
dependencies:
- base >= 4.7 && < 5
- hspec
- QuickCheck
- lens
- network
ghc-options:
- -Wall
- -Wcompat
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wredundant-constraints
- -fno-warn-unused-do-bind
This turns on the following warnings: Wall
, Wcompat
, Wincomplete-record-updates
, Wincomplete-uni-patterns
, and Wredundant-constraints
. This isn't C++, if you see a warning you should fix it. You should try to never check in code with warnings to version control. This is under the section ghc-options
because these are just regular options passed to GHC - you don't have to build your project with Stack, you could call GHC manually, but Stack handling everything for you makes things much more convenient and reliable.
Next up is the topic of GHC language extensions. Haskell is a very nice language but sometimes it has some room for improvement through unofficial extensions implemented in GHC. To do this, we're going to add another section beneath ghc-options
, called default-extensions
. You can turn an extension on by writing a special comment at the top of your source files, but this turns them on for all files and is much more convenient. This is a long list of extensions, and lots of them are fairly complicated so I'm not going to explain them all yet. This list is from Alexis King, and some of the extensions are too complicated for me to easily explain here. Most will be addressed through the course of this book, though, so stay tuned. If you're interested in what a particular extension does, you can look it up on the GHC docs.
ghc-options:
- -Wall
- -Wcompat
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wredundant-constraints
- -fno-warn-unused-do-bind
default-extensions:
- BangPatterns
- ConstraintKinds
- DataKinds
- DefaultSignatures
- DeriveFoldable
- DeriveFunctor
- DeriveGeneric
- DeriveLift
- DeriveTraversable
- DerivingStrategies
- EmptyCase
- ExistentialQuantification
- FlexibleContexts
- FlexibleInstances
- FunctionalDependencies
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NamedFieldPuns
- OverloadedStrings
- PatternSynonyms
- RankNTypes
- ScopedTypeVariables
- StandaloneDeriving
- TupleSections
- TypeApplications
- TypeFamilies
- TypeFamilyDependencies
- TypeOperators
- TemplateHaskell
- QuasiQuotes
You may have noticed we have 3 different files here, stack.yaml
. package.yaml
, and irc.cabal
. Here's the good news: you don't have to worry about irc.cabal
. In fact, you can delete it if you want (although it will reappear). Let me explain.
In the beginning, there was just GHC. If you wanted to compile a program, you passed a list of all the files and all the settings to GHC, and GHC gave you an executable. Of course, this isn't very good from a usability perspective. Because of that, Cabal was created. Cabal is a program which reads a file like irc.cabal
and uses the information inside that file to tell GHC to build a program. You can tell it what options you want to provide GHC, what files to build, what your dependencies are, etc. and Cabal would automatically manage it all for you. But Cabal had lots of issues, and a poor user interface, so Stack was created. This added an additional file, stack.yaml
, where you could write some extra information and then Stack would handle calling Cabal for you, and downloading packages, running tests, and some other things. You rarely need to mess with stack.yaml
, thankfully. But then some people realized that Cabal's file format was actually pretty bad - it's verbose and often requires you write the same thing multiple times. That's why the Hpack file format was created - it's just a file named package.yaml
which gets converted to a .cabal
file in a fairly straightforward process. This means you can edit the much nicer package.yaml
instead of the messier irc.cabal
. In practice, you'll spend most of your time editing package.yaml
, occasionally editing stack.yaml
, and you should never have to edit irc.cabal
.
When you want to add a dependency, you put it in the dependencies
section in your package.yaml
. Stack then looks in a library called "Stackage LTS" for a library by that name and downloads that one if it finds it. There are multiple versions of Stackage LTS, and the one Stack looks at is determined by the resolver in stack.yaml
. The reason for this complexity is for the goal of reproducible builds. You don't write the version number of the library you're using, you only write the name, and there's only one version of each library of any particular instance of Stackage LTS. If you'd like to use a different version, or a version not in Stackage LTS, you must also specify a version number in stack.yaml
. You're essentially saying "if you have a need for this package, use this version". If you forget to do this, Stack will fail to build since it won't know which version to use, but it will give you a nice error message that will typically tell you what to add to the extra-deps
section of your stack.yaml
in order to fix it. In practice, most libraries you need should be in Stackage LTS so you don't have to edit stack.yaml
too much.
Hopefully, this cleared up some lingering confusion you may have had. Now that we've explained that, let's write our IRC client!
The nice thing about having automatic tests for all your code is that Stack makes it very simple to develop this way. You can add the --file-watch
flag to your stack command, and this will cause Stack to automatically rebuild your code and rerun your tests every time you change your code. The full command with everything we've added is as follows:
stack build --test --fast --haddock-deps --file-watch
If you've been writing tests for all your functions, preferably before you even implement them, this becomes a very nice way to develop. Another option is to open your project in stack ghci
and run your code with main
, and reload your code with :r
when you make a change. You can also run your actual project after you've built it with:
stack exec irc-exe
Change irc
to something else if your project is named differently. Eventually, there will be a stack run
command which should remove the need to build before running.
The general process for debugging in Haskell should be:
-
Observe a bug.
-
Figure out how to reproduce the bug consistently.
-
Try to think about what functions are involved in that bug.
-
Think about what those functions inputs and outputs should be in the case that causes the bug.
-
Start writing tests for how your function should work - when these tests fail, you've probably identified one bug or another.
-
Change your functions to fix whatever was causing the tests to fail.
-
Check if the bug still exists. If so, return to step 4.
One very helpful tool is the trace
function. Inside Debug.Trace
, the trace
function takes a String
and a value and returns that value. But invisibly in the background, it also prints the String
you passed in. There is also traceShow
which takes any value in the Show
typeclass instead of a String
. If you want to see if a function is being called, you can replace its output with trace "yep, it's being called" <output>
. If a function is returning x
, you can see that with traceShow x x
. This is common so you can use traceIdShow
which just prints and returns whatever you pass it.
!quickref(Generalized Algebraic Data Types is basically an extension of the Algebraic Data Types we've already seen. They allow us to encode much richer information into types, allowing us to have even more assurance that our code won't break unexpectedly. To use them, we need to enable a GHC language extension with {-# LANGUAGE GADTs #-}
.)
We're getting into some advanced Haskell features here. It's our first foray into Haskell's language extensions, and we're starting off with a good one. It's called Generalized Algebraic Data Types, or GADTs for short. Hopefully you remember data types, they look like this:
data Bool = False | True
Here, Bool
is a type and False
and True
are values. Instead of plain values, you can also have data constructors, like so:
data Currency = Dollars Double | Yen Double | Euros Double deriving (Show)
Now Dollars
is a function which takes an Int
and returns a value of type Currency
.
Prelude> Dollars 2
Dollars 2.0
We can pattern match on these, which is what makes them useful. Let's write a convertToDollars
function.
-- convertToDollars.hs
!include(haskelltests/should_compile/convertToDollars.hs)
We can use this to convert any Currency
to Dollars
.
Prelude> convertToDollars (Yen 3000)
Dollars 26.7
To reiterate, Yen
, Euro
and Dollars
are special functions called data constructors. In our case, they take a Double
and return a value of type Currency
.
So that's is data constructors, but we can also have type constructors. These are pretty useful, especially for situations like Maybe
. Here's how Maybe
is defined:
data Maybe a = Nothing | Just a
Maybe
is a type constructor, which is similar to a a function but it takes a type and returns a type. Just
is a data constructor, which takes a value of type a
and returns a value of type Maybe a
.
Prelude> :t Just "hello"
Just "hello" :: Maybe [Char]
This should seem pretty familiar. We've also discussed recursive data structures. A recursive data structure can contain itself, which is useful for making data structures that can be infinitely large, such as a List. Here's how we'd do that to make a data structure capable of holding a list of Int
s.
data IntList = End | Cons Int IntList deriving (Show, Read, Eq)
IntList
is the name of our type. There are two ways to construct one. The first is with just a plain End
.
Prelude> End
End
Prelude> :t End
End :: IntList
The second is by passing Cons
an Int
and another IntList
.!marginnote(Cons
stands for constructor.)
Prelude> Cons 3 End
Cons 3 End
Prelude> Cons 3 (Cons 4 End)
Cons 3 (Cons 4 End)
Prelude> Cons 3 (Cons 4 (Cons 5 End))
Cons 3 (Cons 4 (Cons 5 End))
Notice that our list always ends with End
. Of course, we can make our IntList
work with all types by using an algebraic data type.
data List a = End | Cons a (List a) deriving (Show, Read, Eq)
Which is very convenient, since it saves us from having to make a new list type manually every time we need one.
Prelude> Cons 'a' End
Cons 'a' End
Prelude> Cons 3 End
Cons 3 End
This is actually very similar to how lists in Haskell are actually implemented! But they use :
instead of Cons
and []
instead of End
.
Prelude> 3:[]
[3]
Prelude> 3:4:5:[]
[3,4,5]
Type constructors take some types and return a new type, and GADTs drastically increase the power of type constructors. To test it out, we need to tell GHC that we want to enable GADTs
. We do this by adding something called a language pragma to the top of our file. In practice, we just write {-# LANGUAGE <Extension> #-}
at the top of our file. Our extension is GADTs
, so we use {-# LANGUAGE GADTs #-}
!marginnote(We can also specify this in our package.yaml
, which makes the extension available to our whole project without having to write any special extensions.). The most basic thing we can do with GADTs is have what's called an uninhabited type, a type with no values. These are pretty much only useful with GADTs so they're not available outside of that.
-- GADTsTest.hs
!include(haskelltests/should_compile/GADTsTest1.hs)
Uninhabited
is a type that contains no values!marginnote(Uninhabited
actually does contain the special value undefined
, which is contained by all types. But we'll ignore undefined
for now.) This means Uninhabited
is almost entirely useless. Functions need to do two things: take and return values. If a function takes a value of type Uninhabited
, we can never call it, because we'd have to pass it a value of type Uninhabited
and there aren't any. And if a function returned type Uninhabited
, there'd be no way to even write it! For this reason it's called a phantom type, a type which exists but we can't use it to write any useful functions.
But we can use phantom types in a useful way, with type constructors! Those take types, not values, so the fact that our type doesn't have any values isn't an issue. Maybe
is a type constructor which takes one types, so we can have Maybe Int
, Maybe Char
, and even Maybe Uninhabited
. We can actually make a value of type Maybe Uninhabited
, too!
Prelude> :l GADTsTest1.hs
*Main> myStrangeValue = (Nothing :: Maybe Uninhabited)
Maybe
takes a value of type a
, but we don't need a value of type a
to make a Nothing
, so this works (although there's still not a good reason to use the type Maybe Uninhabited
anywhere).
A common use of GADTs is to use an uninhabited type as a "tag" for another type, which is possible because the GADTs extension allows us to use where
to make data constructors more powerfully than we could before. This allows us to put more information into the type system, which results in safer functions and fewer bugs in our code. To demonstrate, we'll write a new List
type which will use two phantom types as tags to keep track of whether the list is Empty
or NonEmpty
. We can then use that to write a version of head
which will only work on nonempty lists.
-- SafeHead.hs
!include(haskelltests/should_compile/SafeHead.hs)
Don't worry about the specifics of where
but just yet, let's jump to actually using List
.
Let's make a List
of Char
s.
Prelude> :l SafeHead.hs
*Main> myList = Cons 'a' (Cons 'b' End)
myList
is a value of type List
. The List
contains two elements, 'a'
and 'b'
. But if you use :t
to inspect the value of myList
, you can see it's got the type NonEmpty
in its second parameter.
*Main> :t myList
myList :: List Char NonEmpty
Our function safeHead
takes a value of type List a NonEmpty
. This means we can pass it myList
.
*Main> safeHead myList
'a'
It successfully extracted the first type! safeHead
is pattern matching against Cons a b
, and returning a
. This works because a
will be the first value in our list, and b
will be the rest of the list (which is Cons 'b' End
). Running safeHead End
would give us a compile error, which is exactly what we want. Any time you can make your code give you a compile error instead of a runtime error is a win!
Now, let's dig into the actual type definitions.
data Empty
data NonEmpty
data List a tag where
End :: List a Empty
Cons :: a -> List a tag -> List a NonEmpty
data Empty
and data NonEmpty
are pretty simple - they're phantom types which contain no values. They're used so we can "tag" our List
type with useful values. The interesting definition is actually the List
type, because it uses some syntax we haven't seen before. Earlier in this chapter we defined List
with data List a = End | Cons a (List a)
, where End
and Cons
were the data constructors. GADTs lets us do this in a more powerful way, with where
. We still define two data constructors, End
and Cons
, but because we're using the new where
Syntax we get to specify the output type of our data constructor.
Before, End
was a list of type List a
. Now, End
is a list of type List a Empty
.
Before, Cons
took an a
and a type List a
and returned a List a
. Now, Cons
takes a value of type a
and a value of type type List a tag
and returns a value of type List a NonEmpty
.
This may all seem very confusing, so don't worry if it seems a bit hazy. There's another language extension that makes GADTs much more useful, called DataKinds
, which we'll introduce now and hopefully reinforce GADTs. It makes it very easy to write a safe tail
function, which is what we'll try next.
DataKinds
lets us use a value where Haskell expects a type by putting a '
in front of it!marginnote(It also lets you use a type where haskell expects a kind, which is like the type of a type. We won't be discussing kinds just yet.).
So for example, instead of saying a function takes an Bool
, we could say it takes a 'True
!marginnote(GHC will sometimes allow us to leave out the '
even when referring to True
as a type, but it's best practice to leave it in.). Haskell will then check at compile time that this function never gets passed a False
. This becomes very useful when combined with GADTs because it makes it allows us to put a lot of useful information into our types.
-- SafeHeadTail.hs
!include(haskelltests/should_compile/SafeHeadTail.hs)
Now, what on earth is going on here? Well, first we make a simple recursive data type called Nat
. This is short for natural number, which is a counting number like 0, 1, 2, 3, etc. Instead of having it encoded like that though, we make representations of these numbers with the data constructors Zero
and Succ Nat
(Succ
is short for successor). We can stack as many Succ
s on to a Zero
as we want to make any natural number.
Prelude> :l SafeHeadTail.hs
*Main> one = Succ Zero
*Main> four = Succ (Succ (Succ one))
Then we have our definition of List
, which uses n
instead of tag
. n
is going to represent a Nat
, which will represent the length of our list. End
is much the same as before, except instead of NonEmpty
we use 'Zero
. Now, observe Cons
because it's very interesting:
Cons :: a -> List a n -> List a ('Succ n)
It takes an a
, and a List a n
, and returns a new List a ('Succ n)
. Essentially it adds an extra Succ
to the value which represents the length of our list! This means our lists start at length Zero
and then get a Succ
added to them every time we grow the list by one element. This means we can use information about the length of our list in type signatures, as we do in safeHead
and safeTail
!
safeHead
's type signature is safeHead :: List a ('Succ n) -> a
. The ('Succ n)
will make it so it won't work if our list's n
is Zero
, but will work if it's Succ Zero
or Succ (Succ Zero)
. This is serving the same purpose as NonZero
in the last example.
safeTail
's type signature is safeTail :: List a ('Succ n) -> List a n
. This is the thing we couldn't do when we only had Empty
and NonEmpty
. SafeTail
returns a list of all the values except the first, and it properly represents that the new list is one element shorter (it has one less Succ
).
Let's see these in action!
*Main> :l SafeHeadTail.hs
*Main> myList = Cons 1 (Cons 2 End)
*Main> safeHead . safeTail $ myList
2
Now, what if we made an error and accidentally tacked on one too many safeTail
s? If we were just using tail
this would fail at runtime, but since we've encoded the length in the type system it fails at compile time instead, which is much better!
*Main> safeHead . safeTail . safeTail $ myList
-- compile error, *not* a runtime exception!
This has been just a brief taste of the GADTs
and DataKinds
extensions. They're very powerful and we've only scratched the surface.