Higher order abstractions for repetitive code
Contents:
# Fold
What if we want to sum or concatenate all elements of the list? Pretty simple functions:
0
a + sum t;;
""
a ^ concat t;;
Now again, just like last time, we’re seeing a lot of the same patterns, let’s abstract it away:
init
a some_op combine t;;
What should be the return type when the list is empty? We’ll let the combine function, take in that initial value. And how do we decide what operation? We’ll again take it as a function:
init
f a ;;
Now let’s see, how does the sum look like now?
x + y;;
let sumlist = combine ~init:0 ~f:sum
(*now we should be able to use sumlist, because we just defined it
as a partial function. We could also write it as an anon function
cause why risk readability*)
let sumlist = combine ~init:0 ~f:;;
let a = sumlist ;;
And what about the concat one?
let concatlist = combine ~init:"" ~f:;;
The OCaml library exposes a function called fold which has the same idea as the combine function we wrote.
# fold_right
Now fold has a few variants, one of them is fold_right, that is, it folds the elements of the list from the right to the left. The fold function is also available inside the List module (we’ll study about modules later).
fold_right ~f:f ~init:init
Think of this as accumulating an answer by:
- Repeatedly applying
fto an element of list and the “answer so far” - Computes
f a (f b (f c init))(This is kinda like currying!)
So “apply f to c and init, then apply f to b and the answer to the previous one, then to a and the answer to the previous one.”
Can we code the fold_right algo ourselves?
acc
f a ;;
We’ll start calling the initial value the accumulator. The standard library actually implements it like this:
match lst acc
f a ;;
So, fold_right basically means that you must accumulate the result of the right-most element of the list first. Similarly, there exists a fold_left which accumulates the left-most elements of the list first. Effectively it is f(f(f(init a) b) c).
acc
let acc' = f acc a (*This is what we have acculumated so far, first accumulate the left part*)
in
(*We could then write this as*)
acc
fold_left f t
When the operation is associative, folding from the left or right doesn’t make a difference. But its for those other operators where it does make a difference!
fold_left is tail_recursive! So, compiler can optimize it.
Note that we can reverse a list by using fold_left… (and its naturally tail-recursive)
fold_left [
# Filter
Let’s now consider the task of keeping some elements in a list, while dropping others (that is, filtering):
(*This is a function that would keep only even elements of the list*)
[
(*This is a function that would keep only odd elements of the list*)
[
Now again we can notice a very similar pattern here, so we should do that. We’ll probably need a filtering condition, a function that takes in a value and decides whether to keep it or not. It can simply give me a boolean.
[
(*Or a bit syntactically simplified*)
[
And that’s the filter version I came up with! (I haven’t watched what the professor did yet). But then I can use this version as:
filter ~f: ;;
filter ~f: ;;
Note that filter is not tail recursive because there is still extra work left to do :(. How do you generally build a tail-recursive function? You use an accumulator:
match lst acc
aux f [ lst;;
- Add an accumulator to the main function call
- Simplify the main call to a normal function and create an auxliary recursive function inside it
- The base case changes to return the accumulator
- Just call the aux function, then find a way to fill the accumulator inside it, with an expression
One interesting observation with this is that, if we run it like this, we get the output but in the reversed order! So, if we want to fix that (think about it, because we’re accumulting the first few values first, so we’re essentially reversing the list). To fix that we’ll have to return List.rev acc in the base case.
That’s only for the tail-recursive case though.
# Trees with map and fold
So, we have read about using map and fold for lists, now let’s try doing it for trees, creating our usual definitions:
(*A recursive invariant for trees*)
let t = Node
To map a tree (as in, take a function, and apply it to every node of the tree), we can do something like this:
Leaf
Node
That’s cool, it’s going to return another tree with a function f applied to each of its node (we’re only considering a binary tree for now). Let’s create a simply function f to increment the value of each node.
map ~f: t;;
If we trace out the map function in UTop to see what’s happening, we get this:
map <-- f:<fun>
map --> <fun>
map* <-- Node (<poly>, Node (<poly>, Leaf, Leaf), Node (<poly>, Leaf, Leaf))
map <-- f:<fun>
map --> <fun>
map* <-- Node (<poly>, Leaf, Leaf)
map <-- f:<fun>
map --> <fun>
map* <-- Leaf
map* --> Leaf
map <-- f:<fun>
map --> <fun>
map* <-- Leaf
map* --> Leaf
map* --> Node (<poly>, Leaf, Leaf)
map <-- f:<fun>
map --> <fun>
map* <-- Node (<poly>, Leaf, Leaf)
map <-- f:<fun>
map --> <fun>
map* <-- Leaf
map* --> Leaf
map <-- f:<fun>
map --> <fun>
map* <-- Leaf
map* --> Leaf
map* --> Node (<poly>, Leaf, Leaf)
map* --> Node (<poly>, Node (<poly>, Leaf, Leaf), Node (<poly>, Leaf, Leaf))
- : int tree = Node (4, Node (3, Leaf, Leaf), Node (3, Leaf, Leaf))
Let’s also try fold:
acc
f a
This is going to recursively fold over the left and the right subtrees. You can now like, sum up all the elements of the tree if you want:
(*Note how this needs to be taking in 3 params*)
x + y + z;;
let a= fold ~f:sum 0 t;;
Now we’re going to dive into software dev in OCaml!