Notes on Haskell
These are notes from my lectures at university augmented with some online and textbook reading. They’re unfinished but they’ve been sitting in my drafts so I thought I’d publish them in case others find them useful.
Surely there must be a less primitive way of making big changes in the store than by pushing vast numbers of words back and forth through the von Neumann bottleneck. Not only is this tube a literal bottleneck for the data traffic of a problem, but, more importantly, it is an intellectual bottleneck that has kept us tied to word-at-a-time thinking instead of encouraging us to think in terms of the larger conceptual units of the task at hand. Thus programming is basically planning and detailing the enormous traffic of words through the von Neumann bottleneck, and much of that traffic concerns not significant data itself, but where to find it. — John Backus
Contents
- Thinking functionally
- GHCi
- Basics
- Lists
- Expressions and Types
- Functions
- Constrained Polymorphism
- Language Details
Thinking Functionally
Procedural programming closely mirrors the underlying architecture and focuses on how something is done. Declarative programming (including functional) focuses on what the answer is and provides powerful abstraction (generalisation) mechanisms, as well as being concise and expressive.
Functional programming is programming with functions (lol). The most basic component is a function. They determine a unique output for each combination of inputs.
Instead of asking “How do I change the value of a variable?” or “How do I write a loop?”, ask “What value do I want to produce?”, “How can I make it from the input?” “What are the intermediate values?”.
Expressions are just their values and should be interchangeable.
GHCi
GHC is the compiler for Haskell. GHCi is the interactive environment in which Haskell expressions can be interactively evaluated and programs can be interpreted. It’s what you use on the command line to write Haskell. If you want to follow along, you can either download GHCi or use repl.it for a browser-based environment.
Prelude is just what you get for the command prompt when inside GHCi:
Prelude>
GHCi Commands
:load filename … (or :l as a shortcut) — loads modules from specified
files:reload (or :r) - repeats the last load command
:type exp (or :t) - prints the type of the expression
:quit (or :q) - exits the interpreter
Basics
Examples of basic math operations as well as using built-in functions:
Prelude> 2+3
5
Prelude> 2+3*5
17
Prelude> (2+3)*5
25
Prelude> 2^3
8
Prelude> sqrt 2
1.4142135623730951
Prelude> sqrt 2 + 3
4.414213562373095
Prelude> sqrt (sqrt 2)
1.189207115002721
Prelude> max 4 8
8
Prelude> div 13 5
2
Prelude> mod 13 5
3
Prelude> 13 `div` 5
2
Prelude> 13 `mod` 5
3
The last two are examples of infix functions (take note of the weird quotes).
Lists
Prelude> [1, 1+3, 1+3+5]
[1,4,9]
The value of [a..b] is a list of integers starting at a and ending at b.
Prelude> [1..6]
[1,2,3,4,5,6]
Prelude> [2..2]
[2]
Prelude> [6..1]
[]
Prelude> [6,5..1]
[6,5,4,3,2,1]
Prelude> 1:2:[]
[1,2]
Some list related functions:
Prelude> product [3,4,5]
60
Prelude> product [1..7]
5040
Prelude> product []
1
Prelude> length [1,3,5]
3
Prelude> reverse [1,3,5]
[5,3,1]
If you’re wondering why product [] = 1, its because of the identity in the category of multiplication. A practical explanation:
product [1,2,3]
== product [1] * product 2:3:[]
== product [1] * product [2] * product 3:[]
== product [1] * product [2] * product [3] * product []
which allows us to implement it with simple recursion:
product [] = 1
product (x:xs) = x * product xs
In Haskell, strings are just lists of characters:
Prelude> ['H', 'e', 'l', 'l', 'o']
"Hello"
Prelude> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
Prelude> length "Hello"
5
Prelude> reverse "Hello"
"olleH"
[char] is synonymous with String.
Expressions and Types
Types
Every expression in Haskell has a type.
Unlike some languages like Java and Pascal, Haskell has type inference. If we write a number we don’t have to tell Haskell it’s a number. It can infer that on its own, so we don’t have to explicitly write out the types of our functions and expressions to get things done.
Types are sets of values, and Typeclasses are sets of Types.
For example, the type Integer includes values like 3 and 8, and the Typeclass Num includes the Types Integer and Double.
This is somewhat analogous to Java, where Classes are sets of Objects, and Interfaces are sets of Classes:
Checking the type of 3 gives us a weird answer:
Prelude> :t 3
3 :: Num a => a
Here, a is called a type variable. Everything before the => is called the class constraint. The above says “there is some type a in the typeclass Num, 3 is of type a.” The value of 3 belongs to every type in the Num typeclass, including Integer and Double. Haskell is just being lazy and not committing to a specific type.
Functions also have types:
Prelude> :t head
head :: [a] -> a
This says for any type a, the function head takes a list of as and returns an a. (head returns the first element of a list).
We can even ask for the type of a plus operator. We surround it with parentheses since its an infix operator (meaning we usually use it between values: 2 + 2, otherwise GHCi will get confused and think we’re actually using it ):
Prelude> :t (+)
(+) :: Num a => a -> a -> a
This says: “There is some type a in the typeclass Num, “+” takes two as and returns another a.
How about the function zip:
Prelude> :t zip
zip :: [a] -> [b] -> [(a, b)]
Zip takes a list of as and a list of bs and it returns a list of (a,b) pairs.
We’re allowed to specify the type of a function along with its definition, which is generally considered good practice:
add :: Integer -> Integer -> Integer
add x y = x + y
On the first line, we explicitly tell Haskell that the add function we’re about to define takes two Integers and returns an Integer. Then we define the function on the second line.
Being explicit about types provides some documentation for us and anyone who reads our code, but it also helps us find bugs. For example, say we have the following function:
dividesEvenly x y = (y / x) * x == y
This function is meant to tell us if x evenly divides into y. So if x=2, and y=5, we would expect (5/2) to be 2, since 2 goes into 5 twice, then 2*2 would give us 4. 4==5 is not true so we would expect this function to return False given 2 5 as arguments. However, it returns True.
Prelude> dividesEvenly 2 5
True
This is because, in Haskell, 5/2 is not 2, but 2.5. One way to address this is to specify that we expect x and y to be Ints:
Prelude> dividesEvenly :: Int -> Int -> Bool
Prelude> dividesEvenly x y = (y / x) * x == y
However, this gives us an error:
<interactive>:3:1: error:
• No instance for (Fractional Int)
arising from a use of ‘dividesEvenly’
It is telling us that Int is not a member of the typeclass Fractional. The / operator only works on Fractionals. Turns out the operator we want is `div`:
Prelude> dividesEvenly :: Int -> Int -> Bool
Prelude> dividesEvenly x y = (y `div` x) * x == y
Prelude> divideEvenly 2 5 False
By specifying the type of our function, we were able to find a bug that we may have otherwise missed.
Functions
Infix Operators
Haskell permits the usual infix notation:
Prelude> 1 + 2*3
11
The infix operators +
and *
refer to functions that take two arguments. Operators also have precedence and associations. To refer to these kinds of functions by themselves, we wrap them in parentheses, writing (+)
and (*)
. So we can use them like this:
Prelude> (*) 2 5
10
Prelude> (+) 1 ((*) 2 5)
11
If we have ordinary identifiers that refer to binary functions:
div :: Int -> Int -> Int
mod :: Int -> Int -> Int
we can use them as infix functions by wrapping them in backquotes:
Prelude> (1987 `div` 100) `mod` 4
which is the same as:
Prelude> mod (div 1987 100) 4
Functions on Int
(+) :: Int -> Int -> Int
(-) :: Int -> Int -> Int
(*) :: Int -> Int -> Int
(^) :: Int -> Int -> Int
div :: Int -> Int -> Int
mod :: Int -> Int -> Int
abs :: Int -> Int
negate :: Int -> Int
Examples:
Prelude> abs 3
3
Prelude> negate 3
-3
Prelude> abs (negate 3)
3
Prelude> negate (negate 3)
3
Prelude> abs 0
0
Prelude> negate 0
0
Relational Operators
(<) :: Int -> Int -> Bool
(<=) :: Int -> Int -> Bool
(>) :: Int -> Int -> Bool
(>=) :: Int -> Int -> Bool
(==) :: Int -> Int -> Bool
(/=) :: Int -> Int -> Bool
Examples:
Prelude> 2 < 3
True
Prelude> 2 < 3
True
Prelude> 2 < 2
False
Prelude> 2 /= 2
False
Prelude> 2 /= 3
True
Overloading
The (==)
function has several types:
(==) :: Int -> Int -> Bool
(==) :: Bool -> Bool -> Bool
(==) :: Char -> Char -> Bool
(==) :: Float -> Float -> Bool
(==) :: Eq a => a -> a -> Bool
(<=) :: Ord a => a -> a -> Bool
Building Boolean expressions
(&&) :: Bool -> Bool -> Bool
(||) :: Bool -> Bool -> Bool
not :: Bool -> Bool
Examples:
Prelude> not True
False
Prelude> False || True
True
Prelude> 1 < 2 || 1 > 2
True
A function using relational operators:
small :: Int -> Bool
small n = 0 <= n && n < 10
Functions between types
Haskell has no implicit conversion between types. You must use explicit functions:
To use the ord
and chr
functions, you must import the Data.Char
library module:
Prelude> floor 3.7
3
Prelude> ceiling 3.7
4
Prelude> :m + Data.Char
Prelude> ord 'a'
97
Prelude> chr 98
'b'
Functions of Char
isDigit :: Char -> Bool
isDigit c = ’0’ <= c && c <= ’9’
isUpper, isLower :: Char -> Bool
isUpper c = ’A’ <= c && c <= ’Z’
isLower c = ’a’ <= c && c <= ’z’
isAlpha :: Char -> Bool
isAlpha c = isUpper c || isLower c
ord :: Char -> Int
chr :: Int -> Char
Examples:
Prelude> :m + Data.Char
Prelude> isDigit '2'
True
Prelude> isLower 'B'
False
Prelude> ord '1'
49
Prelude> chr 65
'A'
Prelude> chr 48
'0'
Prelude> chr 32
' '
Prelude> chr (ord 'b')
'b'
Prelude> chr (ord 'b' + 3)
'e'
Functions on Float
(+) :: Float -> Float -> Float
(-) :: Float -> Float -> Float
(*) :: Float -> Float -> Float
(/) :: Float -> Float -> Float
(^) :: Float -> Int -> Float
abs :: Float -> Float
negate :: Float -> Float
sin :: Float -> Float
asin :: Float -> Float
exp :: Float -> Float
log :: Float -> Float
sqrt :: Float -> Float
Guarded Definitions
maxTwo :: Int -> Int -> Int
maxTwo x y
| x >= y = x
| otherwise = y
The Boolean expression x >= y
is called a guard. Guards are tested in order, so if we had:
maxThree :: Int -> Int -> Int -> Int
maxThree x y z
| x >= y && x >= z = x
| y >= x && y >= z = y
| otherwise = z
it could be simplified to:
maxThree :: Int -> Int -> Int -> Int
maxThree x y z
| x >= y && x >= z = x
| y >= z = y
| otherwise = z
Conditional Expressions
Haskell also has an expression form: if boolexp then exp1 esle exp2
.
So instead of:
maxTwo :: Int -> Int -> Int
maxTwo x y
| x >= y = x
| otherwise = y
we could write:
max :: Int -> Int -> Int
max x y = if x >= y then x else y
You can also define functions using existing functions. So you could turn this:
maxThree :: Int -> Int -> Int -> Int
maxThree x y z
| x >= y && x >= z = x
| y >= z = y
| otherwise = z
into this:
maxThree x y z = maxTwo x (maxTwo y z)
Local Definitions
You can have local definitions which are further indented and must line up:
sumSquares :: Int -> Int -> Int
sumSquares n m
= sqN + sqM
where
sqN = n*n
sqM = m*m
You can use local definitions in all guards, as well as the right-hand expressions:
maxsq :: Int -> Int -> Int
maxsq x y
| sqx > sqy = sqx
| otherwise = sqy
where
sqx = sq x
sqy = sq y
sq :: Int -> Int
sq z = z*z
Constrained polymorphism
For example, the “element” function takes two arguments and asks “does the first argument occur in the second argument (which is a structure of some kind)? And it works for multiple types:
Prelude> elem ‘e’ “Hello”
True
Prelude> elem 3 [1..5]
True
This function is almost polymorphic: the types of things being compared must support equality testing, which is reflected in the constrained type of elem:
Prelude> :t elem
elem :: (Eq a, Foldable t) => a -> t a -> Bool
Here, Eq a is a constraint on the type of a. The type a must belong to the typeclass Eq: those that support equality testing such as Char, Int and Integer.
Language Details
Indentation
In files, Haskell uses indentation to decide where another definition starts, so all definitions must have the same indentation. The following would be seen as two definitions and is legal:
square
:: Integer -> Integer
square n = n * n
The following are not legal, Haskell sees the first as one definition and the second as two definitions:
square :: Integer -> Integer
square n = n * n
square
:: Integer -> Integer
Subscribe to my Substack to be notified of future posts.