Lecture 02
2023-04-05 | Week 1 | edited by Ruining Ding
(originally written 2022-09-28 by Matt Wang)
Ruining here! This continues from last lecture, and covers the first 36 slides of the Intro to FP deck. As always, please give me feedback on the notes!
Table of Contents
What is Functional Programming?
Functional programming is defined by several key tenets:
- every function must take an argument (or more than one)
- C-type
main()
functions are forbidden
- C-type
- every function must return a value
- C-type functions of type
void
are forbidden
- C-type functions of type
- functions are pure: they should have no side effects
- side effects include changing global variables, input/output, print statements, etc.
- the most important tenet!
- calling a function
f(x)
with the same argumentx
should always return the same outputy
(Ruining will call this determinism) - variables are immutable: after declaration, they cannot be modified
- ex:
f(x){ x = x + 1; return x }
is not allowed!
- ex:
- functions are first-class - just like other values, they can be stored as variables, passed as arguments, etc.
More rigorously:, a pure function has two core properties:
- It doesn’t depend on any data other than its inputs to compute a result
- It doesn’t modify any data beyond initializing local variables required to compute its output
Sometimes, this is called a referentially transparent function.
This function is pure: it only depends on p
, and the only variable it initializes is never re-assigned (i.e., it’s just to help us with naming).
int f(int p) {
int q = 5 * p * p;
return q;
}
This function is not pure: not only does it depend on a global variable z
(not an input to the function), but it also modifies it during the computation!
int z;
int f(int p) {
return p * z++;
}
There are a few consequences resulting from these basic tenets:
- no random numbers can be generated or used
- since that would violate determinism
- no input is taken in a compiled Haskell programme
- since that would violate determinism (input will determine result produced)
- no output is produced (aside from the final product) in a compiled Haskell programme
- since that would not longer be pure (we are altering program state)
Functional versus Imperative
Let’s compare and contrast!
Imperative programs:
- are sequences of statements, loops, and function calls - they operate sequentially
- require changes in variable state
- as a result, multi-threading can be tricky, since there’s shared mutable state and thus race conditions
- the order of execution matters!
Functional programs have different paradigms:
- at the core, functional programs are only composition of functions - no loops, statements, etc.
- no changes in variable state are allowed
- multi-threading is trivial: there is no shared mutable state (since mutability is not allowed)!
- and, interestingly, the order of execution is not important
What does that last point mean? Under the hood, it’s all about side effects.
In an imperative language, calling functions in different orders can have side effects. Consider this example:
int global = 0;
int func1(int arg) {
return arg + global++;
}
int func2(int arg) {
return arg * global;
}
int func(int arg) {
int y = func1(arg);
int z = func2(arg);
if (z == 7)
return 0;
else return y;
}
In func
, even though the declarations of y
and z
seem to not depend on each other - calling them in different orders matters!
- if we were to run
int y = func1(arg)
first, thenglobal
gets incremented to1
, which changesz
’s behaviour! - if we were to run
int z = func(arg)
first, thenglobal
stays at0
, andz
returns0
!
Functional programming disallows code like what we just saw; so, we really could evaluate y
and z
in any order!
Intro to Haskell
Haskell is one of the first (and earliest) pure functional languages. This means that it is one of the best imbodiments of functional programming design goals with minimal features from other paradigms. While it has many restrictions - like requiring generally pure functions - it really lets us learn functional programming!
Our First Program
After installing Haskell, we can run the interactive interpreter with ghci
:
$ ghci
(you can exit this with Ctrl+D
)
In a separate window, let’s make a file - hypot.hs
(note the file extension)
-- hypot.hs
hypot a b = sqrt((a^2) + (b^2))
--
is how you declare a comment in Haskell.
We can now load this in ghci
:
$ ghci
GHCi, version 9.2.4: https://www.haskell.org/ghc/ :? for help
ghci> :load hypot
[1 of 1] Compiling Main ( hypot.hs, interpreted )
Ok, one module loaded.
ghci> hypot 3 4
5.0
Let’s say we update our file:
-- hypot.hs
square x = x * x
hypot a b = sqrt (square a + square b)
We can reload this file with :r
:
ghci> :r
[1 of 1] Compiling Main ( hypot.hs, interpreted )
Ok, one module loaded.
ghci> hypot 7 8
10.63014581273465
We can also define functions in the interpreter itself:
ghci> quadratic x y = 2 * x^2 + y
ghci> quadratic 3 1
19
We’ll talk more about functions later in this lecture!
The Golden Rule of Haskell Indentation
Code which is part of an expression should be indented further in than the beginning of that expression.
You must align the spacing for all items in a group.
If you don’t follow this rule, you’ll get some errors!
Examples:
mult x y =
x * y
let
x = 5
y = 6
-- note: this is rather uncommon
let x = 5
y = 6
where
x = a
y = b
where x = a
y = b
case x of
42 -> foo
-1 -> bar
if foo
then bar
else boo
Data Types in Haskell
The Type System
Haskell is a statically typed language. That means that the type of all variables and all functions can be figured out at compile time
(this is different from Python - a dynamically typed language - where types can only be determined as the program actually runs)
Haskell also has type inference: the compiler can figure out the types of most expressions without you annotating them! Neat!
(this started in functional programming, and has made its way into mainstream languages like C++’s auto
keyword)
Primitive Types
The built-in data types are:
Int
: 64-bit signed integersInteger
: arbitrary-precision signed integers- aside: how does this work?
- in languages like Python, we can treat numbers as arrays of
Int
s of a “Base2^64
” system - matt says, the Wikipedia article isn’t bad!
Bool
: booleans (True
orFalse
)Char
: characters (like in other languages)Float
: 32-bit (single-precision) floating pointDouble
: 64-bit (double-precision) floating point
Let’s see a live code example of some primitive types:
-- example.hs
-- Defining an Int
nerds = 150 :: Int
-- Defining an Integer
googleplex = 10^100 :: Integer
-- Defining a Float
rootbeer = 3.14159 :: Float
-- Defining a Bool
carey_has_hair = False :: Bool
When we enter just the name of a variable, ghci
will print its value for us:
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> googleplex
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
:t
tells us the type of the expression passed to it. This is really, really useful: it’s a helpful tool to learn more about the language and its type inference rules!
ghci> :t googleplex
googleplex :: Integer
Operators
Haskell supports operators you’re used to: +
, -
, *
, /
, ^
, etc.
But, there’s a few caveats!
This works well:
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> 42 + nerds * 10
1542
In Haskell, the /
division operator does not work on Integers! (why might that be - there’s a good functional reason!)
ghci> nerds / 10
<interactive>:3:7: error:
• No instance for (Fractional Int) arising from a use of ‘/’
• In the expression: nerds / 10
In an equation for ‘it’: it = nerds / 10
You must use `div`
instead.
ghci> nerds `div` 10
15
However, /
works on floats just fine:
ghci> rootbeer / 10
0.314159
To make numbers negative, you need to add a parentheses: this makes -
a unary operator:
ghci> nerds + (-1)
149
There is prefix notation for infix operators:
ghci> (+) rootbeer 1.0
4.14159
Boolean operators generally work like how you’d expect!
ghci> nerds > 100 && not carey_has_hair
True
Composite Types
We’ll dive into three composite types:
- a
Tuple
is a fixed-size collection of values; each value can be any type. - a
List
is a sequence of values; each value must be the same type. - a
String
is a list of characters!
Let’s see some examples in action.
Tuples
Tuples are great for associating values together, or having a function “return multiple values”.
--- examples.hs
-- A Tuple for a student's score
grade = ("Sergey", 97)
-- A job posting: title, wage, hrs/wk
job = ("Chef", 35.5, 40)
-- A tuple with tuples, lists & ints
whoa = ((1,"two"),['a','b','c'], 17)
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> grade
("Sergey",97)
ghci> fst grade
"Sergey"
ghci> snd grade
97
fst
and snd
are only defined for tuples with two elements! They do not work for tuples with 3 or more elements - you’d use pattern matching instead (coming next lecture)!
What is the type of the Tuple? Let’s ask ghci:
ghci> :t grade
grade :: (String, Integer)
Interesting! Note that the type of the tuple encodes:
- the number of elements in the tuple
- the types of each element in the tuple
This means, for example, that you can’t have a list of any two tuples - they must all be tuples of the same size and types!
Lists
Lists are the building block of functional programming. Note that they are not arrays: they have O(N)
access and no pre-defined size.
--- examples.hs
-- A List of primes
primes :: [Int]
primes = [1,2,3,5,7,11,13]
-- A List of jobs
jobs = ["SWE","Chef","Writer"]
-- A List of Lists
lol = [[1,2,3],[4,5],[6,7,8,9]]
-- A list of tuples
lot = [("foo",1),("bar",2),("boo",3)]
-- An empty list
mt = []
The most commonly used list functions are head
and tail
; head
returns the first item of the list, and tail
returns a list of the rest of the items!
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> head primes
1
ghci> tail primes
[2,3,5,7,11,13]
Here are some other handly simple list functions.
Is a list empty?
ghci> null []
True
Get the list’s length:
ghci> length primes
7
take
and drop
are convenient ways to access slices of a list.
ghci> take 3 primes
[1,2,3]
ghci> drop 4 primes
[7,11,13]
!!
gives you random access by index:
ghci> jobs !! 2
"Writer"
elem
tells you if an item is in a list:
ghci> elem "Chef" jobs
True
sum
adds up the entire list; there’s also a corresponding prod
ghci> sum primes
42
or
performs boolean or
over the list; there’s also a corresponding and
ghci> or [True, False, False]
True
ghci> and [True, False, False]
False
zip
creates tuples out of two lists. This is the same as python’s zip
!
ghci> zip [10,20,30] jobs
[(10,"SWE"),(20,"Chef"),(30,"Writer")]
Like the name may imply, Haskell lists are implemented as singly-linked lists. List operations are implemented recursively! This has performance impacts, since there is no linear random access!
Strings
You’ve seen Strings before!
-- examples.hs
-- Defining a String w/explicit type
truth :: String
truth = "USC sucks"
-- Defining a string w/o a type
lies = "USC kids are smart"
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> truth
"USC sucks"
We can perform concatenation with ++
:
ghci> "I think " ++ truth
"I think USC sucks"
Like many languages, under-the-hood, String ~= [Char]
(Strings are really just a character list)! This means we can use head
, tail
, and check for equality:
ghci> :t truth
truth :: String
ghci> head truth
'U'
ghci> tail truth
"SC sucks"
ghci> "hey" == ['h','e','y']
True
List Processing
Lists may seem boring, but they form the foundation of functional programming (other than, well, functions). In particular, functional list manipulation has made its way into almost every mainstream programming language!
Creating Lists: Concatenate and Cons
We can make a list using the :
“cons” operator to add an item to the front, or the ++
concatenation operator to join two lists.
-- examples.hs
-- A List of primes
primes = [1,2,3,5,7,11,13]
-- A bigger List of primes
more_primes = primes ++ [17,19]
-- A List of jobs
jobs = ["SWE","Chef","Writer"]
-- A bigger list of jobs
more_jobs = "Prof" : jobs
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> more_primes
[1,2,3,5,7,11,13,17,19]
ghci> more_jobs
["Prof","SWE","Chef","Writer"]
We can also create lists from scratch - but don’t forget the trailing : []
!
ghci> 1 : []
[1]
ghci> 1 : 3 : []
[1,3]
ghci> (10,20) : (30,40) : []
[(10,20),(30,40)]
Creating Lists: Ranges
Ranges are a neat feature in Haskell that let us easily create lists. (You could think of this behaving similarly to when you drag a cell on Excel/Google Sheets to repeat a pattern.)
-- example.hs
-- All #s between 1 and 10, inclusive
one_to_ten = [1..10]
-- Odd #s between 1 and 10
oddities = [1,3..10]
-- An infinite list from 42 onward!
whole_lotta_numbers = [42..]
-- An infinite cycle of 1,3,5,1,3,5,…
tricycle = cycle [1,3,5]
ghci> :load examples
[1 of 1] Compiling Main ( examples.hs, interpreted )
Ok, one module loaded.
ghci> one_to_ten
[1,2,3,4,5,6,7,8,9,10]
ghci> oddities
[1,3,5,7,9]
ghci> take 5 whole_lotta_numbers
[42,43,44,45,46]
ghci> take 10 tricycle
[1,3,5,1,3,5,1,3,5,1]
Woah … what’s going on with infinite lists? That can’t possibly work, right?
Well … it does! In particular, this goes back to Haskell being lazily-evaluated. Haskell won’t generate list items from a range until they’re needed: so, if you never ask for list element #1000 out of the infinite list, Haskell won’t care! In other words, none of the list is initialized when it’s initially declared.
You may then ask some questions:
- can you take the
tail
of an infinite list?- yes, you can! for example,
tail [42..]
is the same as[43..]
. You can usetail [42..]
just like[42..]
.
- yes, you can! for example,
- “but Matt”, you say,
tail [42..] == [43..]
hangs, andtail [42..]
just keeps on printing numbers. What’s up with that?- well, when
ghci
returns a value, it calls a “print function” on it; the print function for lists keeps on printing until the list is empty. - so, when you type in
tail [42..]
, Haskell will keep printing until the list is empty, which is … never. Hence, wall of text! - on the contrary,
tail [42..] == [43..]
hangs because of the implementation of the==
operator, which compares heads until the list is empty. - since this never happens, the program doesn’t terminate!
- well, when
- what is the
length
of an infinite list?- try it :)
Be careful when trying it! In particular, Haskell (and all programming languages) cannot always tell if your program goes into an infinite loop. Have your Ctrl+C
ready!
List Comprehensions
The Idea
List comprehensions are a compact yet extraordinarily powerful syntax to create lists.
Semiformally, here’s a definition:
A list comprehension is a F.P. construct that lets you easily create a new list based on one or more existing lists. With a list comprehension, you specify the following inputs:
- One or more input lists that you want to draw from (these are called “generators”)
- A set of filters on the input lists (these are called “guards”)
- A transformation applied to the inputs before items are added to the output list.
Carey’s created this animation that maps some Python-like pseudocode into what a Haskell list comprehension looks like on slide 34.
Comprehensions can also use multiple input lists and guards! (As seen on slide 35.)
Hi, Ruining here. If you happen to be familiar with set-builder notation from math classes, comprehensions are similar (see section on programming languages for a side-by-side comparison).
Simple Examples
Let’s walk through the simple examples:
-- comp.hs
-- Generate squares of 2 thru 7
squares = [x^2 | x <- [2..7]]
-- Generate products of two lists
prods = [x*y | x <- [3,7], y <- [2,4,6]]
-- Generate combinations of strings
nouns = ["party","exam","studying"]
adjs = ["lit","brutal","chill"]
combos = [adj ++ " " ++ noun |
adj <- adjs, noun <- nouns]
-- Generate combinations of tuples
menu = [(oz,flavor) |
oz <- [4,6],
flavor <- ["Choc","Earwax","Booger"]]
ghci> :load comp.hs
[1 of 1] Compiling Main ( comp.hs, interpreted )
Ok, one module loaded.
ghci> squares
[4,9,16,25,36,49]
ghci> prods
[6,12,18,14,28,42]
ghci> combos
["lit party","lit exam","lit studying","brutal party","brutal exam","brutal studying","chill party","chill exam","chill studying"]
ghci> menu
[(4,"Choc"),(4,"Earwax"),(4,"Booger"),(6,"Choc"),(6,"Earwax"),(6,"Booger")]
And that’s it from Ruining and Matt! Let Ruining know if you’ve got any q’s :)