Guide to Haskell for the Absolute Beginner
Table of Contents
- 1. Overview
- 2. Basic Basics
- 3. How a program makes a computer do stuff, at the level of electronics and stuff
- 4. Compilers
- 5. MATH
- 6. Turing vs. Church and Why Haskell?
- 7. DONE What is Haskell? (lazy, statically-typed, pure functional)
- 8. SUPER Basic Syntax and running Haskell interactively with
ghci
- 9. Comments
- 10. DONE "Strings"
- 11. DONE Lists
- 12. DONE Layout rule
- 13. DONE Editors
- 14. DONE Unit testing with Hspec
- 15. Haddock (documentation comments)
- 16. DONE Hoogle
- 17. DONE Data type constructors, conditionals, guards, pattern-matching, and more function stuff
- 18. DONE Currying
- 19. DONE Basic functions that show up a lot
- 20. Script idea: inverting a list of spices and what they can be used for, from email messages
1 Overview
So, I got a wild hair and wondered what an introduction to Haskell would look like to someone who doesn't really computer at all, except to use a laptop or smartphone to surf the web and write emails and social media posts.
Said hypothetical person is intelligent, has an interest in programming (or, at least, finding out how far their interest extends), but is also a bit overwhelmed or intimidated by the knowledge required and the way knowledgeable people tend to present it.
Maybe I can do better. Let's go. :) (Hmm, maybe I should find some emojis for this static web site thingie.)
1.1 For readers who might already be familiar with programming
In case you got here and you already program in something like Java or Python, you can still read this doc, but skip straight to the stuff that's specific to Haskell and is almost certainly different from what you're used to:
- Turing vs. Church and Why Haskell?
- What is Haskell? (lazy, statically-typed, pure functional)
- SUPER Basic Syntax and running Haskell interactively with
ghci
- Comments
- Strings: You can skip this section so long as you know strings (denoted with "") are just lists of characters (denoted with '').
- Lists and everything else to the end of the document.
2 Basic Basics
Wait, before I head into syntax, I should back up a few steps.
Computers are programmable machines, meaning, they take in input, examine the input, make decisions1, and then produce outputs. Sometimes the inputs come from other hardware (like an airspeed sensor, say), and the outputs go to more hardware (like a wing flap actuator, say).
But, also, sometimes the input comes from a human and the output goes back to a human (maybe even the same human).
The process of making the same machine respond to different inputs in different ways might be referred to as "programming". (Unless you're a hardware person and you just design the machine to act different in different circumstances [like, say, when the power switch is on, it makes noise, but when the power switch is off, it doesn't], but we don't care about that right now.)
3 How a program makes a computer do stuff, at the level of electronics and stuff
So, a computer's hardware does input and output as a bunch of tiny voltage changes (just "high" and "low" – we don't even care about the difference between 2.2 volts and 2.3 volts, unless one is "low" and the other is "high") on a bunch of wires. Those wires get hooked up to more electronics and hardware like the switches under keyboard keys and electrical motors and stuff, but we don't care about that. We just care about the tiny incoming and outgoing voltages.
So, the computer has instructions like:
- Take the current set of (8, say) input voltages and save them on a bunch of other internal
electronic circuits called "flip-flops" (because, as long as the circuit has power, the amount
of current running through a flip-flop is steady, until another voltage is applied to a part of
the circuit, and then the flip-flop changes state, and has, say, NO (or very little) current
running through it. So it flip-flops).
- We design these flip-flops to be super-responsive and fast (which also makes them expensive), and we call them "registers" because they register (save) data. Like a cash register, which is all mechanical: you push a key and a flag pops up and stays up, until you push another key to lower the flag.
SO ANYWAY…
- Take the current set of 8 input voltages and save them on this register. Maybe we call the
register "A" for no good reason apart from the fact that it's the first letter of the alphabet,
but also… "accumulator" starts with "a", huh.
- By the way, each of these eight tiny voltages (high or low) is called a "bit", which is a
shortening of "binary digit" because (a) who wants to say all that all the time, and (b)
really, it's just a bit of data, right?
- Also, eight bits is a byte, because who wants to say "eight bits" all the time, when "byte"
will do? Plus, it scares the uninitiated.
- Oh, wait, there's MATH. You can go read that now and then come back here, because it's about to get important.
- Also, eight bits is a byte, because who wants to say "eight bits" all the time, when "byte"
will do? Plus, it scares the uninitiated.
- By the way, each of these eight tiny voltages (high or low) is called a "bit", which is a
shortening of "binary digit" because (a) who wants to say all that all the time, and (b)
really, it's just a bit of data, right?
- Take this other set of 8 voltages sitting out in circuits in memory (which is slower and cheaper than registers) and store it another register. Maybe we call that register "B". (Incidentally, "base" starts with a "b".)
- Subtract B from A (remember the MATH?), being sure to set flags indicating that we had to do a carry from some imaginary 9th bit. (Or a borrow. But "carry" and "borrow" are pretty much the same, really.)
- If we had to carry (i.e., B was bigger than A), skip the next step.
- Go to the end of the program (remember, we might be skipping this step).
- Send some output voltages that turns on a light.
- THE END. Ta-da!!!
Except in "real life", it looks more like this:
INP A LOAD B 1000 CMP B JC END LOAD C 1001 OUTP C END:
And, man, is that ugly. Who wants to do that? (Well, maybe people who write code for fighter jets and washing machines because it has to be as small and fast and tight and cheap as possible, but apart from those guys….)
That's why we have compilers.
4 Compilers
So, we want to write our instructions to the computer in something besides gibberish. The stuff above is called "machine language" or "assembler language" (there's a difference, but we can take them to be the same for now). It's low-level, at the level the machine "understands"2. We want a higher-level language. There are lots of them, for various types of problems:
- BASIC
For beginners. A BASIC program to match the above program might look like this:
A = INPUT 1 B = 152 IF A < B THEN OUTPUT 2
- FORTRAN
- For science-y number-y type stuff.
- COBOL
- For business stuff like accounts receivable and payroll ledgers.
- LISP
- For people who think everything is a list and computers can be made to appear intelligent if they can just process all the lists of stuff we have in our heads.
- Pascal
- For people who are tired of immense blobs of BASIC code.
- C
- For people who want to get code working on completely new hardware with a minimum of screwing around with interpreters (like for BASIC) or compilers (for Pascal). And who also hate Pascal's constant stream of compiler errors when they try to do stuff like subtract a decimal number from an integer.
- C++
- For people who are tired of immense blobs of C code that constantly break when you make one tiny change.
- Java
- For people who are tired of all the memory-management errors in C++ programs.
- C#
- This is pretty similar to Java, but it's for people who love Microsoft so much that they can't see anything not invented by Microsoft. Or whose bosses tell them "we're going with Microsoft because that way, people won't have to know so much to produce results. Also, it's pretty."
- Haskell
- For people who are tired of dealing with errors caused by subroutines that have undocumented side effects, and are also ready to work at a higher level than just slinging data around (they want to try slinging functions).
We call the programs that translate programs written in the above high-level languages into machine language compilers, because they compile the code. Admiral Grace Hopper gets credited with that word, because she had a bunch of subroutines floating around she was constantly compiling into whatever new code she wrote, to be loaded onto the machines she was working with.
5 MATH
So, uh, collections of 8-bit bytes can be interpreted as numbers and slung around that way. Get ready for some base-2 (binary) math.
We interpret a high voltage as a 1 (like, the number 1) and a low voltage as a 0. If we string 8 of them together in order, we get what looks like a number:
10101100
And, like decimal numbers (ones place, tens place, hundreds place, …) the order of the digits is important. And, like decimal, where the ones place is how many ones do you have (and one is just 10^0, because anything raised to the 0-th power is just 1 (except 0, maybe, what is 0^0? I dunno)), and the tens place is how many tens do you have (and ten is just 10^1) and the hundreds place is how many hundreds to you have (and a hundred is just 10^2) and so on, binary is the same way, except we use 2 as the base instead of 10.
So, the question is: how many ones do you have (where one is just 2^0) and how many twos do you have (where two is 2^1) and how many fours do you have (where four is 2^2) and how many eights do you have (where eight is 2^3) and how many sixteens do you have, and so on.
And you can see that every digit can only be 0 or 1, because if it was 2, it would just carry over to the next place, right? So, like, if we had two ones, that's really just one two (stick with me), and if we had two twos, that's really just one four, and so on, right?
So, the number above is (and, of course, we have to go backwards, because how else are you going to learn to ride a unicycle?)…
\(0 \times 2^0 + 0 \times 2^1 + 1 \times 2^2 + 1 \times 2^3 + 0 \times 2^4 + 1 \times 2^5 + 0 \times 2^6 + 1 \times 2^7 = 172\)
a.k.a. (in case your browser doesn't display MathJax):
0 ✕ 2^0 + 0 ✕ 2^1 + 1 ✕ 2^2 + 1 ✕ 2^3 + 0 ✕ 2^4 + 1 ✕ 2^5 + 0 ✕ 2^6 + 1 ✕ 2^7 = 172
172!
Most of the time, we don't care, except when we do. Now you can go back to reading wherever you were before.
6 Turing vs. Church and Why Haskell?
I have to talk about this for a moment, because it kind of helps address the "Why Haskell?" question.
6.1 Turing
Everybody's heard of Alan Turing (right?). Father of modern computing or some such. He was a mathematician in the olden days before electronics, so all his thoughts of computers were in his head (or literally on paper). There are a couple of things that make him a big deal:
6.1.1 Turing machines
Turing imagined a machine that had an infinitely-long tape (like, a paper teletype tape, like a ticker tape) and a finite set of basic instructions like "read whatever's on the tape at the current position", "move the tape forward or backward one position", "write some piece of data from a register onto the tape", "subtract two numbers in memory", "if there was a carry, skip the next instruction", etc. (This is from memory, so if I got it wrong, sue me.)
6.1.2 Computability
Well, strictly speaking, this wasn't Turing. This was a bunch of other people, but the idea was (is) that all computers can be shown to be equivalent to a Turing machine, so if a problem can be shown to be solvable on a Turing machine, any modern computer can solve it. (No statements are made about how long a solution might take, and this leads to more interestingness that I'm not going into now.)
6.2 Church
So, while everybody's going on and on about Alan Turing, there's this other guy, named Alonzo Church, who was roughly contemporary with Turing. He came up with a form of math called "lambda calculus". It's all functions.
So, while Turing is inventing a machine that stores state on a tape (with assignment statements, basically) and computes that way, Church is inventing a form of math that "stores" state in mathematical function results.
6.2.1 Church-Turing Thesis
The electrifying thing is the idea that lambda calculus can do everything a Turing machine can and vice versa. (This hasn't been proven, but everybody pretty much accepts it as true, so I do, too.)
6.3 Why Haskell?
So: computing without assignment statements. What's an assignment statement? you ask.
Remember that BASIC code above?
B = 152
That's an assignment statement. We're assigning the value 152 to whatever area of memory B
represents. This is like a Turing machine scribbling on its tape.
That's all fine and dandy, but what if you modified the above program to call a subroutine
between the time you assigned the value 152 to B
and the time you used B
, and said
subroutine modified B
without telling you it would?
A = INPUT 1 B = 152 REMark The following function modifies B but the documentation doesn't say anything about that, REMark nor can we read the code because we bought it from another company CALL SPIFYRTN IF A < B THEN OUTPUT 2
So now, we loaded 152 into B
, and we happen to know the input to the program was 12 (because
we measured it with a voltmeter), so the OUTPUT 2
statement should have turned on the
light, but it didn't! What's wrong?
After screwing around for a day, we finally think to check B
at the time of the IF
statement, and we find that, lo and behold, it's not 152 as we thought, but 0! Because the
SPIFYRTN
call changed it behind our backs! (With its own not-easily-visible assignment
statement. Not a very spiffy routine at all.) Good thing we found this in testing, because if
we had shipped this code, that light not lighting up is the "patient is having a heart attack"
light, and we could have killed someone.
This is a big deal, because assignment statements lead to an enormous class of bugs (basically, undocumented subroutine side effects). So, imagine how great it would be if we could write programs without assignment statements, and not even have these sorts of bugs. That's Haskell (and a bunch of other functional languages like OCAML and F# and Scala, but Haskell is kind of the granddaddy).
6.3.1 A tiny bit about Haskell
Haskell the language was named after yet another old-timey math guy (a logician, actually) whose name was Haskell Curry. I don't know what he's famous for.
The primary (the Swedes would probably say "hold on there, hoss, you mean a primary") place
where Haskell research and language compiler development takes place is the University of
Glasgow, in Scotland. The Haskell compiler we'll be using is the Glasgow Haskell Compiler, and
the main command to run the compiler is ghc
. (The people in Glasgow call it the Glorious
Haskell Compiler, though.)
It's possible to run an interpreter (kind of an instant-feedback compiler), and that command
is ghci
.
You can get started with the entire shebang at https://haskell.org. Download and install the "Haskell Platform" and you'll be off to the races.
7 DONE What is Haskell? (lazy, statically-typed, pure functional)
- CLOSING NOTE
So, what is there about Haskell to appeal to the geek?
Haskell is a lazy, statically-typed, pure functional programming language.
Breakdown:
7.1 Lazy
Haskell doesn't do any computations until it really needs to. You can set up the most monstrously-complex computation and Haskell will only evaluate it when it really needs to, even if it looks like it should evaluate. Seriously, Haskell only waits until it REALLY needs to do the computation.
Suppose I define a function that looks like this:
f x y = if (x < 0) then y else x
Meaning, the function gives the value y if x is negative, otherwise it just gives the value of x.
Then, suppose I define some other horribly complex and expensive function h and call f like this:
f 2 (h 12)
Meaning (you might think), "calculate the value of \(h(12)\) (let's call the result \(z\), for no good reason), even though it's horribly complex, and then call \(f(2, z)\). Since the first argument is 2, we get the 2 back, so… why did we pay the cost of computing \(h(12)\)?
Haskell doesn't do that. It's lazy. It puts off the calculation until it really needs it.
Here's a cool example:
Prelude> :{ Prelude| let g :: Int -> [a] -> Int Prelude| g x ys = if (x < 0) then (length ys) else x Prelude| :} Prelude> g 2 [1..10] 2 Prelude> g (-2) [1..10] 10
Spiffy, right? If x is negative, it gives the length of the list ys, otherwise it just gives x.
What if we hand it an infinite list (you can do that; see Infinite lists). Now we're expecting it to count the length of an infinite list. That's like that Star Trek episode where Spock asks the Evil Computer to compute the last digit of pi. It ain't never gonna come back.
Except… if we hand the function a positive first argument, it doesn't even need to count the length of the list, and it doesn't.
Prelude> g 2 [1..] 2
Boom.
Just for grins, let's see what happens when I use a negative number:
Prelude> g (-2) [1..] ^CInterrupted.
(I got impatient after, like, 7 seconds, because I knew it wasn't coming back.)
7.2 Statically-typed
Haskell knows the types of everything before the program starts running. And you can't hand something of the wrong type off to something that expects it to be right type.
This is different from languages that are dynamically typed. In a dynamically-typed language, if I declare a numeric function and hand it a string like "2", we expect the function to Do The Right Thing and convert the "2" to 2 and go from there. If we hand said function a string like "onyx", it'll try to convert "onyx" to a number and almost certainly bomb out, at run time. (Or, worse, decide "onyx" is really 0, and sail merrily on, giving what looks like a correct calculation. What if I gave it "5even"? Too bad, eh?)
7.2.1 Type inference
Along with the static typing comes something called "type inference", which means you can be sort of casual about your declarations and Haskell will do a pretty good job of figuring out what you really need.
For example, suppose we define said numeric function like this:
Prelude> f x = x^2 Prelude> f 9 81
(So, f squares numbers.)
And then we ask Haskell what the type of f is:
Prelude> :t f f :: Num a => a -> a
What Haskell is saying here is that f is a function that takes some type a and gives the
same type, so long as a is a number (Num
) of some sort. So, integer, floating point, Roman
numeral (so long as you define the math on that puppy), tally marks, whatever.
How did it figure out that a needs to be a number? We used the exponentiation operator (^
),
which is a mathematical operator. (Maybe I should have used +
here to make things simpler.)
7.3 Pure
Here's where things start to get really interesting. "Pure" means Haskell has no assignment statements. It has no exceptions that say "well, in this special case, you can assign a value to a variable."
7.3.1 Memoizing
That puritanical stance against assignment statements has some nice outcomes. For one, whenever you call a function, since there are no side effects, you can rely on that function giving the same results for the same arguments. In fact, that allows Haskell to simply cache the result, so, for an expensive function, the first time you call it (and really need it), you pay the price, but from then on, for the same arguments, Haskell just gives the value it memorized. So, you get a nice performance bump.
There's an actual verb for that, and it's not "memorize". Instead, we say memoize, like that bishop in Princess Bride.
7.3.2 Parallelizing
(That was awkward, but I wanted to match "Memoizing".) If there aren't side effects to functions (like a function trying to scribble on a global variable), then you can break work up into chunks and do the chunks in parallel. Say, you have a list of a million words and four CPUs and you want to sort the list. You break it up into four chunks and hand the job of sorting each chunk off to each processor and then merge the results when they're done.
That's a little bit of a dumb example, but the point is: if a function has side effects, you can't easily parallelize it, because then the same side effect would happen multiple times, and that would probably be Bad.
7.4 Functional
And, finally, Haskell does everything with functions, slinging them around pretty much with gay abandon, as in: "here's a function, I don't know what it is (except it's mathematical); call it on each number in this list, please". Or: "here are two functions, I don't know what they are (except they're mathematical), please compose them together and call the composition on this list of numbers".
(You probably remember function composition from your math days: \(f(g(x)) = (f \circ g)(x)\). That "\(\circ\)" is the composition operator you know and love.)
8 SUPER Basic Syntax and running Haskell interactively with ghci
Ok. So, now that we've established all that, and you've read down to here, I'll assume you're ready and interested to learn some Haskell.
Haskell code looks pretty simple, at first. If you fire up ghci
and type an expression, it'll
evaluate the expression and tell you the result.
So, if you type 2
, you get 2
. And if you type "Hello!"
, you get "Hello!"
. You can also
type expressions, like 2 + 3
and 2 * 3
, which is addition and multiplication.
Like this:
deimos$ ghci GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci Prelude> 2 2 Prelude> "Hello!" "Hello!" Prelude> 2+3 5 Prelude> 2 * 3 6 Prelude> :q Leaving GHCi.
(Deimos is the name of my computer (a Mac). This is all happening at a command prompt (no mouse clicking for you!), which Windows users sometimes refer to as "the black window". You can get to it by holding down the Windows key and hitting "R" and then typing "cmd" in the little text input field and hitting the "Enter" key. Surely you have done something like this before. You can also use the Start menu to open a Console window, it's the same thing.)
8.1 Functions (Simplest)
And you can define functions. Haskell functions look different from math functions. Math functions look like this:
\(c(x) = (x - 32) / 1.8\)
Or, for those w/out MathJax:
c(x) = (x - 32) / 1.8
That function converts Fahrenheit to centigrade, if you're interested. The inverse function being:
\(f(x) = x \times 1.8 + 32\)
Haskell functions don't have the parentheses. They just use spaces. In fact, when you see two things separated by spaces in Haskell (that aren't explainable by normal syntax rules), it's almost always a function being applied to an argument.
So, if we were to apply the function c
to the value 22 (°F), it would look like this:
Prelude> c 22 -5.555555555555555
So, like, -6 °C. No parentheses. You could use them, but they'd be useless. Parentheses are
used like in regular math, to prioritize math operations that would normally be low priorities, as in
the definition of the function c
above.
Defining that function in Haskell looks kind of the same:
Prelude> c x = (x-32)/1.8
(Try it! I know you already installed Haskell, didn't you?)
And you can convert centigrade back to Fahrenheit, so when Midnight Oil sings "boiling diesels steam in 45°" (https://youtu.be/jpkGvk1rQBI), you can know how hot that is.
Prelude> f x = x * 1.8 + 32 Prelude> f 45 113.0
Ok, that's it. That all. Now you can use Haskell to balance your checkbook. Just fire up ghci
and start entering some mathematical expressions.
Prelude> 1800-750 1050 Prelude> 1050-850 200 Prelude> 200-30 170 Prelude> 170-250 -80 Prelude> -80-350 -430
:(
I'm guessing you didn't need Haskell for that, though.
8.2 Special note on using ()s around negative numbers
By the way, while we're on the topic of negative numbers, it's best to surround them with parentheses, e.g.
(-80) - 350
We got away with no parens in the example above, but in more-complicated situations, you'll see weird errors:
Prelude> 2 * -3 <interactive>:1:1: error: Precedence parsing error cannot mix `*' [infixl 7] and prefix `-' [infixl 6] in the same infix expression Prelude> 2 * (-3) -6
9 Comments
In programming, a "comment" is a piece of English (or Arabic or Cherokee or whatever is your natural language of choice) text you slap into the middle of a program containing whatever documentation you think will be helpful to other people (including you, 18 months from now) reading and trying to understand your code.
The compiler (generally) ignores the comments. It's like they're spaces or something.
It'll become more obvious later, but there are two ways you can put comments into Haskell programs.
- You can type a double dash ("
--
") and then, everything you type after that, to the end of the line, is a comment. - You can type "
{-
" and "-}
" and put your comment between those two. (And I think you can even nest them.)
10 DONE "Strings"
- CLOSING NOTE
It occurs to me that I casually sling around the word "string" without defining it. It's probably one of the first true technical buzzwords you can learn: it just means a string of characters. That's all.
So, you have characters like 'h', and 'e', and 'l', and 'o'. Typically, characters (single letters or glyphs from whatever alphabet you're using) are indicated with single quotes (and Haskell and most of the other programming languages I mentioned above require the use of single quotes for characters).
And then you have strings, like "hello"
. And those are typically indicated with double quotes (and you're
required to use double quotes by those same languages).
As a funky technical note, "a"
is a string (containing only one character) and 'a'
is a character, and it's
not the same as "a"
.
There's a technique for getting special characters into strings (backslashes), but hopefully I'll remember to mention it later, when I need to.
11 DONE Lists
- CLOSING NOTE
I mentioned two types in the initial intro in Basic Syntax (implicitly): numbers and strings. We
define these things functionally, really. Numbers are things you can do math with, and strings
are things you can read and display. (For example, putStr
is a function that puts a string
(and only a string) to the output.)
There's another common, basic type of data: lists. Lists are collections of data that are all of the same type, and come in some sort of order (first, next, etc., etc., last).
So, [1, 2, 3]
is a list. So is [2, 3, 1]
, and it's different from the first list because the
ordering is different.
A string is just a list of characters, so "hello"
is just the same as ['h', 'e', 'l', 'l',
'o']
.
11.1 Lists specified algorithmically (ranges)
You can list out the contents of a list as above, but you can also specify the contents of the lists a different way. Essentially, you use a recipe.
[1..10]
is a list of all the integers from 1 to 10.
[2,4..10]
is a list of all the even numbers from 2 to 10.
Sadly, you can only go by addition (or subtraction), so you can't, for instance, expect
[1,2..128]
to be a list of all the powers of 2 from 1 to 128. Nor can you give Haskell a hint
with something like [1,2,4..128].
However, there are more tricks!
[ 2^x | x <- [1..10]]
is the aforementioned "powers of two" list. If you go back to your math
days, you can almost read this as:
"THE LIST OF ([
) all 2^x SUCH THAT (|
) x IS TAKEN FROM (<-
) the list of integers from 1 to
10"
You can make the condition more complicated:
[ 2^x | x <- [1..10] , 2^x <
128]= is the same list as above, except we also require 2^x to be
les than or equal to 128.
11.2 DONE Infinite lists
- CLOSING NOTE
You can do this. This blows more-experienced developers' minds, but you can probably be comfortable with this concept.
[1..]
is the infinite list of positive integers. (I don't believe you can have a list be
infinite on both ends, but there might be a trick you can pull if you really want something
like that. All lists have to have a starting point.)
If you try to print that list out, you'll be waiting for a long time for the printing to stop. (You can hit ctrl-C (hold down the control key and hit the 'C' key) to stop it.)
But you can do something like this:
take 10 [1..]
means "take the first ten items from the infinite list of integers".
Which sounds pretty stupid, but it can come in handy sometimes when you have a less-predictable infinite list to deal with.
12 DONE Layout rule
- CLOSING NOTE
This is tough to define (especially when you don't really understand it, which is true in my case), but here's how I think it goes:
- Haskell statements are separated by semicolons.
- Groups of haskell statements are surrounded by curly braces (
{}
).
BUT…
If you put the things separated by semicolons on separate lines, you don't need the semicolons.
And if you indent the things grouped by braces, you don't need the braces.
So…
module Layout where -- 'do' is one of those statements that expects curly braces containing a list of statements. The only such statements -- (or keywords) are: -- -- do -- where -- let -- of -- -- And then, once you're in for a curly brace penny, you're in for semicolon pounds. Meaning, every statement needs to -- be separated with semicolons. -- oneLine = do { putStr "Hello, " ; putStrLn "there!" } -- But, you can put things on separate lines and indent them properly and get away w/out the braces and semicolons. laidOut = do putStr "Hello, " putStrLn "there!" -- A more common alternative laidOut2 = do putStr "Hello, " putStrLn "there!" -- You can do this, too, if you want, but it will bollix everything up that comes after it. badLayout = do putStr "Hello, " putStrLn "there!" -- But once you commit to an indentation, you can't back out of it. Everything else in that block needs to be indented -- the same. illegalLayout = do putStr "Hello, " putStrLn ", there!" -- Same problem here, because that first "putStr" is already "indented". illegalLayout2 = do putStr "Hello, " putStrLn "there!"
13 DONE Editors
- CLOSING NOTE
Speaking of the layout rule, you should get a smarter editor than Notepad, as spiffy as it is. (Or Write or whatever comes with the Mac.)
For beginner types who haven't messed around with programming editors very much (and like "free" as a price), I recommend Visual Studio Code (https://code.visualstudio.com/). (It works just fine on Mac and Linux.) Looks like most of the Haskell plugins are a bit complicated to install, but the simplest one seems to be the one named "Haskell Syntax Highlighting", so try that out. Looks like it does some automatic indenting to kind of give you a hint as to when you need to indent more.
(I have to put in a plug for my editor, emacs, but emacs is a journey of a lifetime. I've been using it for coming up on 40 years now and I still haven't figured out everything about it.)
14 DONE Unit testing with Hspec
- CLOSING NOTE
Speaking of editors and getting set up, you should start early on unit testing. "Unit testing" is testing little units of your work (as opposed to testing the entire program). Unit testing is easier in pure functional languages because to test a function, you just call it. You don't really need to "set stuff up" before you call it because there is no state to be set up. (Parameters for the functions might be complex, though, but it still feels easier than in "imperative" programming languages (which is what the rest of the world uses: "do this, and save the result here; then do that, and save the result there; etc.").
So, if you're writing a function you want to test, don't put code inside your main program to test it. Your main program is not for testing, it's for accomplishing your overall goal, ya know?
Instead, write a side program to do the testing. So much easier then either (1) putting in and taking out code in
your main program, or (2) firing up ghci
and issuing the same manual tests over and over as you make little
changes. And you'll be running multiple tests because either you'll be writing multiple functions or you'll want to
test multiple inputs to your function or both.
Now that I've got you pumped up for unit testing, here's what it looks like.
Say you're writing some program that works with Foos. Put your Foo stuff (data types, functions) in a Foo
module.
Then, write a separate FooSpec
module that looks like this:
module FooSpec where import Test.Hspec import Foo -- Import the module containing the functions you wrote that you want to test. -- This is all black magic at this point. Don't worry about it, just do the -- incantations. The dollar signs are important, so pay attention to them. main :: IO () main = hspec $ do describe "Some readable (English) phrase describing what you're testing in general" $ do it "Some phrase describing a specific single test you're performing, like '1 equals 1'" $ -- Here you write some Boolean (true/false) expression that will be true -- when your test passes, and false otherwise 1 == 1 -- This test will succeed. it "Some other phrase for another test, like '1 equals 2'" $ do 1 == 2 -- This test will fail. it "Tigger tops are made of rubber" $ do getTopMaterial (Foo Tigger) == Rubber it "Tigger bottoms are made of springs" $ do getBottomMaterial (Foo Tigger) == Springs
(I added some silly stuff about tiggers, which, presumably, you defined in Foo
. If you actually want to run all
this, comment out or delete the lines involving Foo and Tiggers, obviously.)
14.1 Install hspec
So, hspec
doesn't come with the Haskell Platform, for some reason, so you'll need to install
it. You can install with cabal
or with stack
. The usage of cabal
is simple, see below.
stack
is better if you're going to do something other than write a proof-of-concept toy, and I
discuss it a little more in Build System in write-a-haskell-program.org.
For cabal
, use the cabal
command (which does come with Haskell Platform) to install it.
Issue the following command at the command prompt:
cabal install hspec
It'll take a while and install a bunch of stuff.
When you compile the above code with ghc, you'll get some incomprehensible warnings about type defaults, but I'm guessing it's because I used actual integers rather than real code to be tested. It's ok; it still runs fine.
14.2 Run the test
Fire up ghci
, load the test module and run its main (or any other test functions (which probably have to have type
IO ()
)):
PS C:\Users\j6l\Documents\AmazonS3\Tarheel-NC\Haskell# ghci .\FooSpec.hs GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from C:\Users\j6l\.ghci [1 of 1] Compiling FooSpec ( FooSpec.hs, interpreted ) [flags changed] Ok, one module loaded. *FooSpec> main Some readable (English) phrase describing what you're testing in general Some phrase describing a specific single test you're performing Some other phrase for another test FAILED [1] Failures: FooSpec.hs:15:5: 1) Some readable (English) phrase describing what you're testing in general Some other phrase for another test To rerun use: --match "/Some readable (English) phrase describing what you're testing in general/Some other phrase for another test/" Randomized with seed 1422829174 Finished in 0.0181 seconds 2 examples, 1 failure *** Exception: ExitFailure 1
You'll see green text for successful tests, and red text for failures (plus the word FAILED
).
15 Haddock (documentation comments)
A quick note about some of the funny comment syntax you might see: like Javadoc, Pydoc, Perldoc, emacs LISP's documentation strings, etc., etc., Haskell has its own documentation-comment syntax, and it's processed by a tool named Haddock.
I won't go into details, but here's what you need to know (and I encourage you to use this in your own code, if you ever get around to writing any):
-- |
is for comments that document the thing that comes after the comment (so, like function
documentation).
-- ^
is for comments that document the thing that comes before the comment (so, like function
argument documentation, where you define the argument and then put the documentation out to the
right).
The space before the "|" or "^" is important.
That is all.
16 DONE Hoogle
- CLOSING NOTE
Hoogle is the Haskell knowledge search engine, your entry point to the deep, dark, shark-infested waters that are the official Haskell library documentation. This documentation is not written to be friendly, but it is written to be comprehensive and correct.
You could, for instance, look up "hspec".
17 DONE Data type constructors, conditionals, guards, pattern-matching, and more function stuff
- CLOSING NOTE
This sounds like an odd combination of topics, but bear with me.
So far, we've seen three data types: numbers, strings, characters. Or four, if you count lists, which is worth counting. As you know, lists can be lists of any type. That's actually worth remembering.
All of our advanced programming languages (after BASIC) are capable of having types of data that
are like agglomerations of other data. For example, you could have a data type of Car
, with year,
make, model, and color. And you could have a function, say, maintenanceCost
, that takes as an
argument a Car
. (All of my examples are stupid, by the way.)
Why do we have these data types? Aren't strings and numbers enough? A license plate is a string. A cost is a number. A loan interest rate is a number. A bank name is a string. A check number is… a number. What more do we need?
Well, suppose we're writing a program for home finance. We have a lot of data floating around. Account balances, transaction amounts, check numbers, dates (are those number? Or strings?), bank names, account types ("Savings", "Checking", "Money Market"). Since all those numbers are numbers, what happens if we accidentally send a check number (2804) to a loan interest calculation ($28.04), and then subtract interest from the checking account balance? Oops.
Wouldn't it be better if we had types like AccountType, CheckNumber, MoneyAmount, Date? You could still make mistakes, but the chances would be reduced, eh?
17.1 A simple data type
Here's a super-simple data type. It's really an enumeration of possible values.
data Direction = North | East | South | West
What's going on here? We're defining a new data type, named "Direction". And it can only have four different values. Simple enough, right?
We could do this with a string, but what if we had a function, bearing
that takes a compass
direction and gives the number of degrees that direction corresponds to? Great, but what if we
asked it direction "Esta" is? ERROR. We can avoid that by using this new data type instead.
Here's the function signature ("signature": the list of input types and the result type a
function has).
bearing :: Direction -> Int
So, the function takes a Direction and gives an Int (an integer).
To be clear about this: we tell Haskell what the type of a function is (its type is its
signature) by giving the function name, a double colon, and then the list of input types and the
result value type, all separated by (short) arrows ("->
").
You might remember, from your days of math in school, that functions are usually specified with arrows. See:
The big difference between Haskell and "regular" math is that, in Haskell, we put arrows everywhere, not just in front of the function result. More about that later. :)
17.1.1 A binary tree
(But I'm not going to go into it any more than I have to.)
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
17.2 Conditionals
17.2.1 if
: Implementation of the bearing
function with conditionals
So, how do we implement this function? How about this:
module Compass where data Direction = North | East | South | West deriving (Eq, Show) bearing :: Direction -> Int bearing dir = if dir == North then 0 else if dir == East then 90 else if dir == South then 180 else 270
(You can see where I'm heading with that "module Compass where
" at the top: unit testing!
Also, that "deriving (Eq, Show)
" is a piece of black magic I haven't talked about yet, but I
will, later.)
So, what did I do? I said "the bearing
function, with a single argument I'm calling dir
, is as
follows:".
Or, in shorter words, "the bearing
function IS….", except I used an equals sign for "is".
That's what "equals" means, right? (More or less.)
Then I have a huge conditional expression. The basic structure of a conditional expression is "=if booleanValue then someValue else someOtherValue".
(By the way, that "boolean" thing. It's just a true/false value, but these things are named after Yet Another Old-Timey Mathematician named George Boole, who invented Boolean algebra and wrote some stuff. It's a short-handed way of saying "true/false value".)
The double-equals sign compares two things for equality and gives a true/false result. So,
"Hi" == "Hi"
is true, and
1 == 2
is false.
The difference a single equal sign and the double equal sign is that with a single equal sign, you're saying that a thing has a certain value, and with the double equal sign, you're asking whether a thing has a certain value.
Anyway, if the boolean value has the value True
, then the entire expression has the value
someValue. Else, the entire expression has the value someOtherValue.
17.2.1.1 Breakdown
Pardon me, I just automatically assumed you, dear reader, would be able to reflexively parse that big if-then-else expression, but maybe I should be a little more pedantic.
Take that last part:
if dir == South then 180 else 270
That's pretty clear, right? If the direction is South, then the bearing is 180°, otherwise it's 270°. Taking that expression in isolation, that "otherwise" could cover a lot (North, East, West), but at least we know it's not South, right?
So, back up a level:
if dir == East then 90 else {- nested 'if' expression -}
(Yeah, I just snuck in a comment, in its more-rare form. I'm being sloppy.)
If the direction is East, then the bearing is 90°. Else it's whatever that nested 'if' expression evaluates to.
And, at the "top", we check for North, and if we got North, we give 0° back. Otherwise, we go down the rabbit hole of nested 'if' expressions, but we know we're not sending North down that rabbit hole. Likewise, in the second 'if', when we hit the 'else' part, we know we've already handled North and East, so we could only be sending in South or West. And, finally, back to the last part's 'else' statement: we already handled North, East and South, so we know it can only be West, right? Which is why we can so confidently say the bearing is 270°.
17.2.1.2 Test
Does it work? To make a long story less long, yes!
deimos$ ghci CompassSpec.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 2] Compiling Compass ( Compass.hs, interpreted ) [2 of 2] Compiling CompassSpec ( CompassSpec.hs, interpreted ) Ok, two modules loaded. *CompassSpec> main bearing North is 0 East is 90 South is 180 West is 270 Finished in 0.0028 seconds 4 examples, 0 failures *CompassSpec> bearing West 270
(By the way, you can see all the code at Compass.hs, and all the unit-test code at CompassSpec.hs.)
17.2.1.3 But, in the end….
But that nested if structure is inelegant. What if we added some more directions to Direction, like NorthEast and SouthBySouthWest, etc.? That line of if/then/else's will march off to the… um… SouthEast.
17.2.2 DONE Guards: another form of conditionals
- CLOSING NOTE
- CLOSING NOTE
There's another way we can put conditionals in function definitions, and you've seen it before in your math textbooks. Check out https://en.wikipedia.org/wiki/Sign_function.
bearingGd dir | dir == North = 0 | dir == East = 90 | dir == South = 180 | dir == West = 270
So, that's prettier. (And we know it works, because I extended the unit test with a little copy-and-paste.)
You basically have "|
", a boolean expression, and then "= theValue".
17.3 Pattern-matching
Ok, here's a much wackier solution (but one you'll see a lot):
bearingPat :: Direction -> Int bearingPat North = 0 bearingPat East = 90 bearingPat South = 180 bearingPat West = 270
What the heck? How can you define the same function more than once? (Maybe you think this is
completely normal. The "bearing" of North is 0 degrees, the bearing of East is 90 degrees, etc.
What's the problem, John?) This is pattern-matching. There are no conditionals here.
Conditionals are ok, but sometimes it's clearer to avoid them. What's going on here is that
we're saying (obviously, I guess) that if the function is called with the value North
, the
function's value is 0, and if it's called with the value East
, its value is 90, and so on.
You will definitely see more pattern-matching in the future, but it'll be a bit more complex (don't worry, not too crazy).
17.3.1 DONE case
: Another form of pattern-matching
- CLOSING NOTE
Here's another way to use pattern-matching w/out the goofiness of "defining" the same function
four times, but beware. It can get ugly, as I'll show down below in the "Maybe
" section.
bearingCase :: Direction -> Int bearingCase dir = case dir of North -> 0 East -> 90 South -> 180 West -> 270
17.3.2 TODO Aliasing in pattern matching ("as patterns")
(This was inserted later, so it might not flow well.)
Sometimes you need (or want) to pattern match on some structure but also refer to the entire thing, not just the piecesparts of the structure. Like, if you want to know something is a list and you need the first element of the list, but you also need the entire list.
We use something called an "as pattern".
listFunkiness allXs @ (_:xs) = xs ++ allXs listFunkiness [] = []
So, allxs
is the entire list and xs
is just part of it (the tail, as it happens).
17.3.3 The difference between pattern-matching and conditionals
You may have figured it out by now (or maybe you've started to get an inkling), but the difference between conditionals and pattern-matching is this:
- With conditionals, you're using a boolean expression that can be as complex as you want but that distills down to either True or False; and
- With pattern-matching, you're checking the
structure of something (does that structure have a certain pattern?). For simple values (like, say
North
), they kind of look alike.
17.4 DONE Maybe
- CLOSING NOTE
Well, mostly done. At least the first pass.
Ok, let's kick it up a notch. There's a built-in data type named "Maybe
". It's essentially
defined like this:
data Maybe a = Nothing | Just a
(There's probably more to the official definition, but this is about right.)
So, we're saying that, like Direction
, a Maybe
value can have one of two possible values:
Nothing
and Just a
.
But, wait, what's the "a
"?
Well, that is Haskellish for "any old thing, of any old type". (The "a" comes from the beginning of the alphabet, not the first letter of "any".) Remember that whole "lists of any type" thing? Same thing.
So, you can have something that's "Just 2
" or "Just North
" or "Just "hello"
".
Why would you do such a thing?
Imagine a function that divides one number by another. What if we passed it zero? What should we give as a result? Should we just blow up the program?
How about this:
divAbyB x 0 = Nothing divAbyB x y = Just (x/y)
(Oh, look! More pattern-matching!)
So, now we're saying anything divided by 0 is Nothing, as opposed to 0.
And, if it's not Nothing, it's Just
whatever the result is.
*CompassSpec> divAbyB 1 0 Nothing *CompassSpec> divAbyB 1 2 Just 0.5
17.4.1 A note on variable names
I'm trying to be clear in my variable names when a variable represents a value (x, y) and when a variable represents a type (a), but you may not be so lucky, normally. (Plus, I may have made it worse by not getting it 100% correct.) Normally, all the variables, no matter what they represent, are just a, b, c, etc., and, after this "Maybe" section, I'll probably stop trying to keep them separate.
17.4.2 Why do we need Just
? And some terminology about "constructors"
Why can't we define the type as
data Maybe a = Nothing | a
Honestly, I don't know. Maybe it's because we wouldn't be able to tell whether a number was a
number or Maybe
a number.
While we're on the topic of stupid niggling details, I might as well tell you what the
"constructor" on the left side of the equal sign is called a "type constructor" (I guess
because it constructs types, like Maybe Int
is a different type from Maybe String
?).
And the constructors on the right side of the equal sign are called "data constructors", because they construct data (like "North" and "Just 12" are data).
17.4.3 Pulling pattern-matching and data types together
Here's why I put data type constructors and pattern-matching in the same section.
Suppose we have the above stupid function, divAbyB
, that might return a Nothing
, and we
write another function, sillyAdd
, that looks like this:
sillyAdd Nothing _ = Nothing sillyAdd _ Nothing = Nothing sillyAdd (Just x) (Just y) = Just (x + y)
That's some more serious pattern-matching, with an extra twist. Now we can pass in the result
of divAbyB
, and be safe. (I would not advise rewriting all of mathematics this way, though.)
*Compass> sillyAdd (Just 2) (divAbyB 1 2) Just 2.5 *Compass> sillyAdd (Just 2) (divAbyB 1 0) Nothing
So, what's happening here? And what's with that underscore character ("_
")? The answer is
that we're pattern-matching on the data type (Maybe
), and the underscore is both a wildcard and a
statement that we don't care about the argument in that position (in the function invocation "f
x y
", the x
and y
are arguments to the function).
So, if we get a Nothing
in the first argument, we don't really care what the second argument
is, we know the function result is Nothing
. Likewise, if we get a Nothing
in the second
argument, we don't care what the first argument is; we know the result is Nothing.
On the other hand, if we don't get Nothing
in either argument, then we know both arguments
are Just
something, and we do even more tricky pattern-matching there. The pattern we're
looking for is that Just
something, because we want to pull the something out of the
Just
, do something with it, and then stick it back in another Just
. And you can see that we
do exactly that. We pull the x out of the first Just x
argument (we use parentheses to keep
Haskell from getting confused about what goes with what), and we pull the y out of the second
Just y
, and then we jam them together with a +
and put the result back inside another
Just
.
Simple!
So, we have another function that Maybe returns a number, but maybe it just returns
Nothing
.
And that's the beauty of pattern-matching.
17.4.3.1 But you can get ugly if you use pattern-matching in case
statements
I said, in the section on case
statements, you can make pattern-matching ugly. Here's how:
sillyAdd2 x y = case (x,y) of (Nothing, _) -> Nothing (_, Nothing) -> Nothing ((Just x), (Just y)) -> Just (x + y)
Gack. We're so determined to have only one "sillyAdd2 x y =" line that we pushed the pattern-matching down into the function body, but…
We need to check both arguments. We can either have nested case
statements or we jam both
arguments together in a tuple (which is basically a collection of disparate pieces of data,
like an ordered pair from your math days (or daze, as the case may be)) and then pattern-match
the tuple.
Quick diversion into tuples here, since I haven't talked about them yet: A tuple is a bit like a list (but it definitely ain't a list!) in that it's a bunch of elements in order, but:
- All tuples "of the same type" have the same length and each element is the same type. So, you can have a tuple like
(3, "hi")
, but if you want to hand that tuple to a function that handles tuples, any tuple you hand that function has to consist of exactly a number and a string, in that order, no more, no less.- They can't be of varying lengths (as I said) and you can't build them up and shrink them down like you can with lists. Not easily, anyway. They just ain't lists. They're more like
Car
's:(2007, "Honda", "Civic", White)
. (WhereWhite
is a value from aColor
type, obviously. Obviously. (:eyeroll:))Anyway, tuples are indicated by parentheses and commas.
Seems to me this is inefficient, since we construct something only to use it in pattern-matching.
17.5 DONE Either
- CLOSING NOTE
You might not be satisfied with a Maybe
data type, because when you get a Nothing
, there's no
info about why you got the Nothing
or what went wrong.
Behold, Either
!
data Either a b = Left a | Right b
So, now you can return the "right" answer or some sinister error info. ("Left" is always bad; we lefties can't get a break.)
divWithError x 0 = Left "division by zero is undefined" divWithError x y = Right (x/y) sillyAdd3 (Left e) _ = Left (e ++ " (sillyAdd3)") sillyAdd3 _ (Left e) = Left (e ++ " (sillyAdd3)") sillyAdd3 (Right x) (Right y) = Right (x + y)
*Compass> sillyAdd3 (Right 1) (divWithError 1 0) Left "division by zero is undefined (sillyAdd3)" *Compass> sillyAdd3 (Right 1) (divWithError 1 2) Right 1.5
17.6 DONE deriving
and common classes
- CLOSING NOTE
I'm going to give this short shrift, because a longer discussion would be a lot longer, but there some useful typeclasses you can use (and will probably have to, if you start defining new types). A typeclass is kind of a type of a type, or a category of types, or a class of types.
So, for instance, the typeclass Eq
is the set of all types can be compared with each other for
equality, and the typeclass Show
is the set of all types that can be converted to
human-readable strings.
You can't use the "==
" operator without being in the Eq
class (normally), and you can't display
a value using putStr
or putStrLn
without being in the Show
class. Also, you can't decide
whether one value is less than another without being in the Ord
("ordinal", "orderable")
typeclass.
So, we usually just slap on a deriving
statement along with a list of typeclasses we want to
invoke.
putStr
, by the way, is a function that puts a string to the output.putStrLn
puts a string followed by an end-of-line sequence (e.g., a carriage return) to the output. You won't use them much in ghci, but you will definitely use them in a lot of regular Haskell programs after you've compiled them to a runnable executable.
17.6.1 Eq
Suppose we left the "deriving Eq
" off our definition of the Direction
type.
module Direction where data Direction = North | East | South | West bearing dir | dir == North = 0 | dir == East = 90 | dir == South = 180 | dir == West = 270
deimos$ ghci Direction.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Direction ( Direction.hs, interpreted ) Direction.hs:6:5: error: • Could not deduce (Eq Direction) arising from a use of ‘==’ from the context: Num p bound by the inferred type of bearing :: Num p => Direction -> p at Direction.hs:(5,1)-(9,23) • In the expression: dir == North In a stmt of a pattern guard for an equation for ‘bearing’: dir == North In an equation for ‘bearing’: bearing dir | dir == North = 0 | dir == East = 90 | dir == South = 180 | dir == West = 270 | 6 | | dir == North = 0 | ^^^^^^^^^^^^ Failed, no modules loaded. Prelude>
Blech. The key part of the above error vomit is this: "Could not deduce (Eq Direction) arising
from a use of ‘==’". We need to derive Eq
.
module Direction where data Direction = North | East | South | West deriving (Eq) bearing dir | dir == North = 0 | dir == East = 90 | dir == South = 180 | dir == West = 270
deimos$ ghci Direction.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Direction ( Direction.hs, interpreted ) Ok, one module loaded. *Direction> bearing East 90 *Direction>
Alles gut.
17.6.2 Ord
Suppose you want to compare some things.
module Grade where data Grade = F | D | C | B | A isPassing grd = grd > F
deimos$ ghci Grade.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Grade ( Grade.hs, interpreted ) Grade.hs:5:17: error: • No instance for (Ord Grade) arising from a use of ‘>’ • In the expression: grd > F In an equation for ‘isPassing’: isPassing grd = grd > F | 5 | isPassing grd = grd > F | ^^^^^^^ Failed, no modules loaded.
"No instance for (Ord Grade) arising from a use of ‘>’". Ok, we know how to fix that.
First attempt at a fix3:
module Grade where data Grade = F | D | C | B | A deriving Ord isPassing grd = grd > F
deimos$ ghci Grade.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Grade ( Grade.hs, interpreted ) Grade.hs:4:12: error: • No instance for (Eq Grade) arising from the 'deriving' clause of a data type declaration Possible fix: use a standalone 'deriving instance' declaration, so you can specify the instance context yourself • When deriving the instance for (Ord Grade) | 4 | deriving Ord | ^^^ Failed, no modules loaded. Prelude>
It turns out that if you want to compare things, they have to be "equalable", too.
module Grade where data Grade = F | D | C | B | A deriving (Ord, Eq) isPassing grd = grd > F
deimos$ ghci Grade.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Grade ( Grade.hs, interpreted ) Ok, one module loaded. *Grade> isPassing B True *Grade> isPassing F False
17.6.3 Show
You'll run into this one a ton:
*Grade> B <interactive>:3:1: error: • No instance for (Show Grade) arising from a use of ‘print’ • In a stmt of an interactive GHCi command: print it
module Grade where data Grade = F | D | C | B | A deriving (Ord, Eq, Show) isPassing grd = grd > F
deimos$ ghci Grade.hs GHCi, version 8.4.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from /Users/john/.ghci [1 of 1] Compiling Grade ( Grade.hs, interpreted ) Ok, one module loaded. *Grade> B B
17.7 DONE Typeclass constraints (=>
)
- CLOSING NOTE
Not super-proud of this section; just sayin'.
(I want to use "typeclass" instead of just "class", because I think that'll be less confusing.)
About that "any old type" bit I mentioned up above in the Maybe
section….
Sometimes, "just any old type" won't do. For instance, you can't do math on strings. And some functions really take only integers, not fractions. And sometimes, data types want to contain only things of certain types, and so on.
In those cases, when you specify function types (which I haven't done much, if any, of so far), you might need to say something like "this function takes any old type so long as it's a number".
f :: (Num a) => a -> a f x = -x
I really don't have any better examples right because I barely know what I'm doing. Check back with me in a year and see if I've made any progress. :/
The concept of a typeclass constraint applies almost anywhere you'd use a data type or a typeclass, in addition to functions.
18 DONE Currying
- CLOSING NOTE
You just got handed a bunch of syntax. Some of it's weird, but it basically all boils down to the sort of syntax any programming language comes with. For example, Python comes with its own layout rule. And Java and SQL have case expressions. Lots of languages have what's called "ternary expressions", which are pretty similar to Haskell's 'if' expression.
But here's some special Haskell weirdness nifty-ness: you don't have to supply every argument to a function.
And, when you don't, you don't get an error or default values for the missing arguments. Instead,
you get another function that takes whatever arguments you didn't feed it.
For example, consider a function that adds three numbers and gives the sum.
module Add3Nums where add3nums x y z = x + y + z
And suppose you only call it with one argument. What you get is a function that adds two numbers
and then adds the original argument you called add3nums
with. So, kind of like add-and-offset.
add2to2nums = add3nums 2
(That was a function definition, by the way.)
*Add3Nums> add2to2nums 3 5 10
And we can do it again, obviously:
add5 = add2to2nums 3
*Add3Nums> add5 17 22
This process of partially applying functions (turning them into other functions with fewer arguments) is called currying, after Haskell Curry.
18.1 Side note: Leaving off arguments in definitions
But, wait, if those are function definitions, where are the arguments? add3nums
has arguments
in its definition, but the other two don't!
It turns out that you can sort of "cancel out" the arguments of a function definition, sometimes.
So, for instance, if I want to define some arbitrary function f, and it's really just the square-root function, I can define it two ways:
f x = sqrt x
or
f = sqrt
It's like, "Well, we know the arguments are going to get slapped on to the end, so let's just agree to leave them off. What do you say, old chap?"
You'll see a bunch of that. Like, "+
" takes two arguments, so if you specify "(+3)
", you've just
defined a function of one argument but you didn't even type a placeholder variable for that last
argument.
Prelude> f = (+3) Prelude> f 2 5
18.2 DONE Function application is the highest priority
- CLOSING NOTE
You remember that whole "My Dear Aunt Sally" thing to help you remember which operations have priority in a mathematical expression. (Multiplication, division, addition, subtraction.)
Exponentiation is higher-priority than multiplication, so \(\frac{1}{8}\times2^3 = 1\), because first you compute \(2^3\) and then you divide by \(8\).
In the same way, function application (function invocation) is the absolute highest priority (at
least, until I remember something that's even higher). So, if you have f 2 + 3
, what's going
to happen is f will be applied to 2, and then the result will have 3 added to it.
Finally, here's what happens (I believe) when you have an expression like f 2 3 4
, where f is
a 3-argument function.
First, f gets applied to 2, yielding an unnamed two-argument function via the miracle of currying.
Then, this unnamed two-argument function gets applied to 3, yielding a one-argument function, also unnamed.
Finally, this one-argument function gets applied to 4, yielding whatever is the end result.
(This is conceptual. In real life, probably something even more mysterious involving a tree of "thunks" happens, but let's just close our eyes and pretend my explanation is good enough.)
18.3 DONE Side note: "$
" is the lowest priority
- CLOSING NOTE
While we're on the subject of function-application being the highest priority operation, it might
be useful to know what the "$
" operator does.
Before we do that, though, let's define some more functions:
module ChainOfFunctions where f x y z = x + y + z g x y = x * y h x = 0 - x main = do putStrLn ("h g 2 f 4 6 8 = " ++ show (h (g 2 (f 4 6 8))))
("++
" is string concatenation, by the way. Jams two strings together.)
PS C:\Users\j6l\Documents\AmazonS3\Tarheel-NC\Haskell# ghci .\ChainOfFunctions.hs GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help Loaded GHCi configuration from C:\Users\j6l\.ghci [1 of 1] Compiling ChainOfFunctions ( ChainOfFunctions.hs, interpreted ) [flags changed] Ok, one module loaded. *ChainOfFunctions> main h g 2 f 4 6 8 = -36
(If you work out all the math, you'll see this is correct.)
That is a ton of parentheses. If you try it without the parentheses, it'll be a glorious mess.
This is because function application takes priority, so, when you have "h g 2 f 4 6 8
", Haskell
tries to apply h to everything that comes after it. So, you have to parenthesize the
application of g, so it gets evaluated before h gets applied, and you also have to
parenthesize the application of f, so it gets evaluated before g gets applied.
AND you have to parenthesize the whole thing before show
gets applied to turn a number into a
string, AND you have to parenthesize all that before putStrLn
gets applied.
But what's a few dozen parentheses among friends? Especially at the end; you can just hammer the right-parenthesis key until the editor is satisfied; that should do the trick.
OR… (see what I did there?4) you could use "$
". "$
" has a very funny definition:
f $ x = f x
Basically, it does nothing but disappear, so what's the point?
The point is that "$
" is defined to have the lowest priority of all the operators. So that
means that when Haskell sees something like f $ 3 + 4
, it says "hold up, I see you want to
apply f to something, but I have to evaluate "3 + 4
" before I can evaluate "f $
<whatever>
", because "+
" has higher priority than "$
"."
So, basically, "$
" means "hold up and evaluate everything from here to the end of the current
expression", which is equivalent to banging in a left parenthesis at this spot and also tacking a
right parenthesis on to the end of the line. "$
" saves parentheses.
18.4 Functions as first-class values
Actually, it turns out that functions are "first class" values in their own right, meaning you can sling them around just like another value. And when I say "functions", I don't mean "function results" or "function invocations" but the functions themselves.
For instance, I can define a function that applies another function to an argument.
applyFunction f = f 2
What's the argument here? A function! We're taking whatever function we're passed as an
argument and applying it to the constant 2
.
*Add3Nums> applyFunction sqrt 1.4142135623730951 *Add3Nums> applyFunction recip 0.5
The square root of 2 is 1.414. And the reciprocal of 2 is \(\frac{1}{2}\). sqrt
and recip
are built-in
functions (that take a single numeric argument), and we passed the functions as arguments to
another function (applyFunction
).
So, a function can exist in its own right, just as the value 2 can exist in its own right.
In fact, you don't even have to give a function a name. This is just like you don't have to give a numeric value a name. If I want to use 12 in an expression, I just do it. So, how does an unnamed function exist?
As a lambda expression.
18.5 Lambda expressions
Why, hello, Alonzo Church! Fancy meeting you here!
A lambda expression looks like this:
\y -> 2 * y + 1
This is a function that takes \(y\), and gives \(2y + 1\) back. Note that it doesn't have a name; it's anonymous.
(How we got from "^
" to "/\
" to "λ
" to "\
" is a fun little story of the abuse of
typography, full of randomness and wonder.5 Basically what it means, though, is that
"lambda" is utterly meaningless, and you don't need to be intimidated by a Greek letter standing
for who knows what abstruse mathematical concept.)
So, we could call applyFunction
like this:
*Add3Nums> applyFunction (\x -> x ^ 3) 8
So, we just applied a cubing function to 2, without bothering to name the function f or g or whatever.
19 DONE Basic functions that show up a lot
- CLOSING NOTE
As I add functions to this list, I realize a lot of them, if not all, operate on lists, usually producing more lists. Which is a bit LISP-ish.
19.1 map
Map a function to each element of a list, producing a list of results.
λ Prelude> ns = [1,2,3] λ Prelude> map (2*) ns [2,4,6]
So we mapped a function that doubles ((2*)
) to a list of numbers, getting a list of doubled
numbers.
We could do the same with a lambda function:
λ Prelude> strs = ["hi","there"] λ Prelude> map (\s -> s ++ " " ++ s) strs ["hi hi","there there"]
(You see the lambda function, right?)
19.2 filter
Filter things out of a list.
λ Prelude> ns = [1..10] λ Prelude> filter (\n -> 0 == (rem n 2)) ns [2,4,6,8,10]
Quick review: ==
is the test for equality. rem
(which you haven't met before) is the
remainder after integer division. So, we're using a boolean lambda expression to filter for
numbers whose remainders are 0 when divided by two (that's bad grammar, but I think you know what
I mean).
19.3 take
You've already met take
in the discussion of infinite lists.
19.4 drop
drop
is a bit like take
, but it drops the first n elements of a list and returns you the
rest of the list.
λ Prelude> drop 3 ns [4,5,6,7,8,9,10]
19.5 ++
++
jams two lists together.
λ Prelude> as = [2,4..10] λ Prelude> bs = [102,104..110] λ Prelude> as ++ bs [2,4,6,8,10,102,104,106,108,110]
You've seen it used to jam strings together, but, remember, strings are just lists of
characters. ++
works on lists of anything (so long as the two lists are of the same thing).
19.6 DONE fold
- CLOSING NOTE
Ah, fold
. This one is trickier. It "folds" a list (or any Foldable
) up into a single
value. You have to give it a starting value, and an operation to use in folding up the list, and
(of course) the list.
There are several variants of fold:
foldl
- Folds "from the left". Computes ( … (((x_0 `f` x_1) `f` x_2) `f` x_3) `f`… ). In other words, first applies function f to the leftmost two arguments, then applies f to that result and the third argument, then applies f to the result of that and the fourth argument, and so on. This is the most straightforward, but it's also a little wasteful of memory, especially when you run it over really huge lists.
foldr
- Folds "from the right". Computes x_0 `f` (x_1 `f` (x_2 `f` (x_3 `f` …))). In other words, applies f to the rightmost two arguments, then applies f to the next argument to the left and the result of the previous application, and so on. Sometimes this is better if your function can arrive at its final result without having to traverse all of an infinite list (boolean functions or multiplication (maybe) can do this if there's a zero somewhere in the list)
foldl'
- Folds from the left like
foldl
, but is more efficient in memory usage so you can use it on a huge list.
(Here's a bit more discussion, if you're interested: https://qr.ae/TW7XD4.)
Some little notes on syntax
A two-argument function (say,
f x y
) can actually be stuck between its operands by using backquotes.Prelude> f x y = x + 2 * y Prelude> f 1 2 5 Prelude> 1 `f` 2 5A single quote is a legitimate part of a variable name. You can read it as "prime". So, you can have a function f, and another function f', read as "f prime". Or, you could have a function named
o'donnell
, if you really want.Prelude> o'donnell x = "Top o' the marning to ye, " ++ x ++ "!" Prelude> o'donnell "Frank" "Top o' the marning to ye, Frank!"
Suppose we want to sum up a list of numbers:
λ Prelude> ns [1,2,3,4,5,6,7,8,9,10] λ Prelude> :t foldl foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b λ Prelude> foldl (+) 0 ns 55
Wut… just happened?
First, we verified that ns
is a list of the integers from 1 to 10.
Then, we reminded ourselves what foldl
wants. And it wants:
- Well, it can only be run over types t that are
Foldable
, for one thing. A list is foldable. - Given that, it wants a function that takes as input things that are type b and type a (two
inputs), and that returns something of type b. In our case, types b and a are the same:
integers, because we're summing a list of integers to an integer. We could be summing up the
lengths of a list of stirngs, in which case, b would still be an integer (the result), but
a would be the type
string
, because our inputs (from the list) are strings. I'll show you that in a minute. foldl
also wants something of type b, our result type. This is the initial value. For summing things, we (usually) start with zero.- And, finally,
foldl
wants thatFoldable
of things that are type a. That's what thatt a
part means. In our case, that means we have to give a list of integers. - And, finally finally,
foldl
returns a thing of type b. An integer, in our case. That's the result of the fold operation.
And then we invoked foldl
. The two-argument function of things type b and a is just
addition ("(+)
"). By putting the "+
" in parentheses, we just handed a raw function to
foldl
. Remember that "functions as first-class values" thing?
Then, the next argument we handed foldl
was 0
, the initial value.
Finally, we gave it the list we want to sum up.
And… out pops the answer! 55!
19.6.1 Summing up the lengths of a list of strings
Here's another example, with some different types at play:
λ Prelude> strs = ["abc", "de", "fghi"] λ Prelude> foldl (\n s -> n + length s) 0 strs 9
This time, our types b and a are different. b is still the return type (an integer), but
now a is type string
(the elements of our list), so the function we give foldl
has to take
as input an integer and a string and return an integer. So, we define a lambda of two arguments
(n
and s
), and return n
plus the length of the string s
. Initialize with 0, and
we're off to the races!
19.6.2 Folding "from the left" vs. "from the right"
If your function is (-)
instead of (+)
, and you run foldr
instead of foldl
, you'll see
the results of folding from either direction.
Prelude> foldl (-) 0 [1,2,3,4] -- ((1 - 2) - 3) - 4 -10 Prelude> foldr (-) 0 [1,2,3,4] -- 1 - (2 - (3 - 4)) -2
20 Script idea: inverting a list of spices and what they can be used for, from email messages
I'm not actually going to write this program; it's an exercise for the reader, but you know how every bottle of spice you have lists what it's good for? And then, when you're cooking, you have no idea what spices to put in your dish? Wouldn't it be nice if you had a program that would spit out all the suggestions from the various spice bottles? Like, your bottle of smoked paprika says "sprinkle on chicken, fish, pork, potatoes or rice", you're like, "hmm, what am I going to season this whitefish fillet with?" (because you've totally forgotten that you have some smoked paprika). Wouldn't it be cool to open your cabinet door to where you've taped this list, and you look up "fish" on it, and see "smoked paprika"?
So, you could email yourself the spice suggestions (maybe with a "spice" subject line or something), find all the emails and save them to text files on your computer, and then process them all with a Haskell program that plucks out the important stuff and builds this list of spices to use with each dish. And, if you keep the output handy, you can feed it back in later, along with some new email messages you sent yourself, and keep adding to it that way.
Input:
spice <spice> use[d] [in|for|with] <dish>, <dish>, <dish> dish <dish> use[s] <spice>, <spice>, <spice> syn[onym] dish = dish
Output will be like the 2nd line of input.
Now, go forth and conquer! :)
Footnotes:
This blew me away when I was a wee nerd in the eighth grade, and it's probably why I got into computers in the first place ("computers making decisions!!!"), but it's not like humans make decisions. We anthropomorphize too much. Outcome of a machine "making decisions" is simply a fork in the program based on inputs. Or a differentiating circuit or something. It's not like the machine considers its choices and comes to a reasoned decision, taking all factors into account. Even so, these sorts of things can Make Life Better(tm), so that's another reason I'm into it.
Oh, look, more anthropomorphization.
By the way, it's worth pointing out that a lot of your work in Haskell will be attempting to fix, to keep the compiler happy. Generally, once you satisfy the compiler, your code will run properly, so that's a silver lining.
Conjunction junction, what's your function?