DEV Community

Yosuf
Yosuf

Posted on • Originally published at yosuf.dev on

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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 []
Enter fullscreen mode Exit fullscreen mode

which allows us to implement it with simple recursion:

product [] = 1
product (x:xs) = x * product xs
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

[char] is synonymous with String.

Expressions and Types


Basic Types in Haskell

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This says for any type a , the function head takes a list of a s 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
Enter fullscreen mode Exit fullscreen mode

This says: “There is some type a in the typeclass Num , “+” takes two a s and returns another a.

How about the function zip:

Prelude> :t zip
zip :: [a] -> [b] -> [(a, b)]
Enter fullscreen mode Exit fullscreen mode

Zip takes a list of a s and a list of b s 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

However, this gives us an error:

<interactive>:3:1: error:    
    • No instance for (Fractional Int)
            arising from a use of ‘dividesEvenly’
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If we have ordinary identifiers that refer to binary functions:

div :: Int -> Int -> Int
mod :: Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

we can use them as infix functions by wrapping them in backquotes:

Prelude> (1987 `div` 100) `mod` 4
Enter fullscreen mode Exit fullscreen mode

which is the same as:

Prelude> mod (div 1987 100) 4
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Relational Operators

(<) :: Int -> Int -> Bool
(<=) :: Int -> Int -> Bool
(>) :: Int -> Int -> Bool
(>=) :: Int -> Int -> Bool
(==) :: Int -> Int -> Bool
(/=) :: Int -> Int -> Bool
Enter fullscreen mode Exit fullscreen mode

Examples:

Prelude> 2 < 3
True

Prelude> 2 < 3
True

Prelude> 2 < 2
False
Prelude> 2 /= 2
False

Prelude> 2 /= 3
True
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Building Boolean expressions

(&&) :: Bool -> Bool -> Bool
(||) :: Bool -> Bool -> Bool
not :: Bool -> Bool
Enter fullscreen mode Exit fullscreen mode

Examples:

Prelude> not True
False

Prelude> False || True
True

Prelude> 1 < 2 || 1 > 2
True
Enter fullscreen mode Exit fullscreen mode

A function using relational operators:

small :: Int -> Bool
small n = 0 <= n && n < 10
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Guarded Definitions

maxTwo :: Int -> Int -> Int
maxTwo x y
    | x >= y = x
    | otherwise = y
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

it could be simplified to:

maxThree :: Int -> Int -> Int -> Int
maxThree x y z
    | x >= y && x >= z = x
    | y >= z = y
    | otherwise = z
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we could write:

max :: Int -> Int -> Int
max x y = if x >= y then x else y
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

into this:

maxThree x y z = maxTwo x (maxTwo y z)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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    
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Subscribe to my Substack to be notified of future posts.

Top comments (0)