Uploaded by eastonmok2

Functional-programming-lecture-jupyter

advertisement
tional-programming-lecture-jupyter
April 25, 2024
0.1
Fun with Functional Programming
0.1.1
Menti
Mentimeter URL: https://www.menti.com/al8ppsou79m9
0.1.2
Lets Explore
• Functions as data
• Anonymous lambda functions
• Common functional programming ideas: map and reduce
0.1.3
Bubble Sort Revisited
In previous lectures we saw how to implement the BubbleSort algorithm:
[1]: def swap(d, i, j):
tmp = d[i]
d[i] = d[j]
d[j] = tmp
def bubbleSort(arr):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
if arr[i-1] > arr[i]:
swap(arr,i-1,i)
swapped = True
Which works as expected:
[1]: data = [1,5,3,7,9,1]
bubbleSort(data)
print(data)
0.1.4
Question: What if I want the list in Descending order?
Seems easy enough, I can just implement bubbleSort again:
1
[1]: def bubbleSortDesc(arr):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE CHANGE OF COMPARISON FUNCITON HERE
if arr[i-1] < arr[i]:
swap(arr,i-1,i)
swapped = True
And that works!
[2]: def swap(d, i, j):
tmp = d[i]
d[i] = d[j]
d[j] = tmp
def bubbleSortDesc(arr):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE CHANGE OF COMPARISON FUNCITON HERE
if arr[i-1] < arr[i]:
swap(arr,i-1,i)
swapped = True
data = [1,5,3,7,9,1]
bubbleSortDesc(data)
print(data)
[9, 7, 5, 3, 1, 1]
0.1.5
Sorting Tuples of Values
Now lets say we want to sort tuples of values (x,y) in ascending order based on the value of y.
I’m going to need another bubbleSort:
[1]: def bubbleSortPairsByYAsc(arr):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE CHANGE OF COMPARISON FUNCITON HERE
if (arr[i-1])[1] > (arr[i])[1]:
swap(arr,i-1,i)
swapped = True
2
And test it:
[4]: def swap(d, i, j):
tmp = d[i]
d[i] = d[j]
d[j] = tmp
def bubbleSortPairsByYAsc(arr):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE CHANGE OF COMPARISON FUNCITON HERE
if (arr[i-1])[1] > (arr[i])[1]:
swap(arr,i-1,i)
swapped = True
data = [(1,5),(3,7),(2,1),(1,1)]
bubbleSortPairsByYAsc(data)
print(data)
[(2, 1), (1, 1), (1, 5), (3, 7)]
0.1.6
Getting Rid of Repetition
There’s a pattern here, to implement a new BubbleSort we need to copy-and-paste the old one and
change the comparison step. The rest of the code is the same in all cases!
In programming we want to:
1. Avoid copy-and-paste as much as we can: it’s easy to mess up, and means if we find a bug in
the original sort we need to change it everywhere
2. Capture common patterns to improve reuse
So we need a way to dynamically change the comparison.
Lucky for us, we have the following core idea:
0.1.7
Core idea: Functions as Data
We can think of functions as just another type of data + integers, floats, dictionaries, functions, …
• Hugely powerful:
– We can pass functions to functions
– Aside: So powerful you can (theoretically) implement any computation with just functions
0.1.8
Generalising BubbleSort
The idea is to turn the comparison step into a function that will return True if two elements should
be swapped
3
[1]: def lessThan(x,y): return x < y
def greaterThan(x,y): return x > y
We can then write a BubbleSort that just uses whatever comparison function we pass.
Aside: This sort of programming, where the exact version of some behaviour is unknown but still
able to be used, is very powerful and the basis of many modern languages! We will see more of this
in the lecture next week!
[1]: def bubbleSort(arr, cmpF):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE USE OF COMPARISON FUNCITON HERE
if cmpF(arr[i-1], arr[i]):
swap(arr,i-1,i)
swapped = True
[4]: def bubbleSort(arr, cmpF):
swapped = True
while swapped:
swapped = False
for i in range(1,len(arr)):
# NOTE USE OF COMPARISON FUNCITON HERE
if cmpF(arr[i-1], arr[i]):
swap(arr,i-1,i)
swapped = True
def lessThan(x,y): return x < y
def greaterThan(x,y): return x > y
def swap(d, i, j):
tmp = d[i]
d[i] = d[j]
d[j] = tmp
data = [1,5,3,7,9,1]
bubbleSort(data, lessThan)
print(data)
bubbleSort(data, greaterThan)
print(data)
[9, 7, 5, 3, 1, 1]
[1, 1, 3, 5, 7, 9]
Really cool!
4
0.1.9
Higher Order Functions
Functions that take functions as arguments are called “higher-order” functions - But there’s nothing
special about them, because functions are just another type of data!
0.1.10
Fixing a Final Annoyance
Having to define comparison functions ahead of time is a bit annoying!
def lt(x,y): return x < y
def gt(x,y): return x > y
def lte(x,y): return x <= y
# ...
But luckily, there’s some special syntax to make this easier
0.1.11
Lambda Functions
• Lambda functions are functions:
– Without a name (are anonymous)
– That can be defined at point of use
Example:
[1]: data = [1,5,3,7,9,1]
bubbleSort(data, lambda x, y : x < y)
print(data)
bubbleSort(data, lambda x, y : x > y)
print(data)
A few things to notice:
1. lambda keyword introduces a lambda function
2. Function arguments are x, and y but don’t need brackets
3. There’s no explicit return needed, it returns the value of the expression automatically
4. Lambda functions (in python) can only be a single expression long; anything longer you need
to use def
0.1.12
Recap
Functions can be used as data, e.g. passed to other functions
Enables programmable control flow: + Algorithm determines general pattern, e.g. of a sort, search
etc + Passed function(s) give specifics
0.1.13
Common Higher Order Functions
Functions as data makes it easy to write succinct operations
Lots of times we want to do something to every element of a list:
[1]: myList = [1,2,3,4,5,6]
newList = []
5
for elem in myList:
newList.append(elem + 1)
Similar question to before: What if I also need to add 2 to a list? What if I want to convert int to
str within the list? …
0.1.14
Map: Do Some Operation On Each Element Of A Structure
Lets have a shot at generalising the above pattern:
[1]: def map(f, data):
res = []
for elem in data:
res.append(f(elem))
return res
Again this is very general: + Focuses on how the iteration is performed
• Doesn’t assume what f does, just that it’s a function (of the right
shape, e.g. 1 input, 1 output)
[2]: data = [1,2,3,4,5]
print(map(lambda x : x + 1, data))
print(map(lambda x : x + 2, data))
print(map(lambda x : str(x), data))
<map object at 0x000002551CF54FD0>
<map object at 0x000002551CF57400>
<map object at 0x000002551CF8EF20>
Aside: you can import functools to get a built-in version of map to avoid defining it each time
0.1.15
Chaining Functions
Notice I put the f argument to map as the first parameter. This was to make it easier to chain
functions together.
For example, we might want to add 5 and then convert to a string:
[1]: data = [1,2,3,4,5]
dataAdded = map(lambda x : x + 5, data)
dataStr = map(lambda x : str(x), dataAdded)
print(dataStr)
Lots of temporary variables to name. Instead we can do:
6
[1]: data = [1,2,3,4,5]
dataStr = map(lambda x : str(x), map(lambda x : x + 5, data))
print(dataStr)
Note the “first” function is the furthest to the right
Aside: In this case using lambda x: str(x+5) is probably better, but for more complex functions
you might need multiple steps
0.1.16
Reduce
Another common pattern takes a list and combines the values in some way
Example: Summing a list
[1]: data = [1,2,3,4,5,6]
acc = 0
for elem in data:
acc += elem
print(acc)
Similar to before we can ask questions like: What if I want to multiply the elements?
Lets generalise using a higher-order function:
[1]: def reduce(f, initialAcc, data):
acc = initialAcc
for elem in data:
acc = f(acc, elem)
return acc
1
There’s a few things to unpack:
• Since we don’t know what operation we are doing we need to pass the initial value of the
accumulator acc, e.g. if we reduce to a Bool type, then this can’t start as 0
• f has to take the “current” accumulator value and somehow add elem into it to produce a
new accumulator
Let’s give it a shot!
[3]: def reduce(f, initialAcc, data):
acc = initialAcc
for elem in data:
acc = f(acc, elem)
return acc
x = reduce(lambda acc, x : acc + x, 0, [1,2,3,4,5,6])
print(x)
x = reduce(lambda acc, x : acc * x, 1, [1,2,3,4,5,6])
print(x)
7
21
720
Neat!
Aside: Reduce is sometimes called “accumulate” or “fold”
1.0.1
Combing Map and Reduce
We can do lots of interesting things by combining map and reduce (and there’s lots of examples of
this in the lab!)
Lets say we want to know if there is a multiple of 3 in a list:
[1]: data = [1,4,6,7,8]
div3 = map(lambda x : x % 3, data)
isZero = map(lambda x : x == 0, div3)
isTrue = reduce(lambda acc, x : acc or x, False, isZero)
print(isTrue)
Or as a nice succinct call (although it’s starting to get a bit harder to read):
[1]: reduce(lambda a, x : a or x, False, map(lambda x : x == 0, map(lambda x : x %␣
↪3, data))
1.0.2
Recap
• Functions are just data so can be passed around/stored as needed
– Functions can take functions are arguments (higher-order functions)
– Allows decoupling flow from specific behaviours, e.g. passing comparison functions to
sorts
• lamba allows us to introduce anonymous functions
– lambda x, y : x + y
– Good for temporary functions, e.g. arguments
• Functions like map and reduce generalise over common patterns:
– map: apply a function to each element of a collection
– reduce: combine the values in a collection to a single result
– And allow for very succinct code to be written
8
Download