Back in 2018 or so, I attended the Advanced Functional Programming summer school at Utrecht University. Doaitse Swierstra was one of the lecturers there and showed this very nifty example of laziness that totally expanded my mind at the time. Last week I came back to the AFP summer school, but as a TA this time, and because, as far as I could tell, none of the lecturers showed this example, I took it upon myself to show it. Googling the specific example, I can find no real reference to it online, so Iāve also taken it upon myself to share it here [0].
With the preamble done, here is the actual problem: Traverse a tree data structure in a breadth-first manner, and replace all the values with their āindexā.
data Tree a
= Leaf a
| Node (Tree a) a (Tree a)
deriving (Eq, Show)
bfLabel :: Tree a -> Tree Int
bfLabel t = ???
But what is the correct LHS of the function here[1]? Itās not trivial at all to implement, and indeed, only one of the students in this yearās batch managed to fill it in at all. One thing we can look at to try and figure it out is the same problem, but depth first.
dfLabel :: Tree a -> Tree Int
dfLabel = fst . go 0 where
go n (Leaf _) = (Leaf n, n+1)
go n (Node l _ r) = let
(l', n') = go (n+1) l
(r', n'') = go n' r
in (Node l' n r', n'')
We need some counter to keep track of the highest label weāve applied so far, and we pass this counter down to a nodeās children as a function argument and pass it up to a nodeās (or leafās) parent as the return argument. This works fine because at each node I really only need 1 value, and I really only need to send 1 value.
Visually, if I have the following tree (bad ms paint incoming):
I have the following counter-path:
but if I tried to use just one counter in a BF case Iād have the following counter path:
generalizing from this specific tree to a more general tree I have, I need to receive arbitrary counters at the top level (from the other leg of the tree) and receive one less counter as I descend down the tree. The same pattern holds for counters I need to send up from the function call. We do have a data structure for arbitrary amounts of values, lists! Except getting the list to use as an argument isā¦ tricky. You might notice that the list that we need to send is exactly the return list of the function, with the seed value 0 attached as its head.
bfLabel :: Tree a -> Tree Int
bfLabel t = let (result, future) = go (0:future) t in result where
go (n:ns) (Leaf a) = (Leaf n, n+1:ns)
go (n:ns) (Node l a r) = let
(l', ns') = go ns l
(r', ns'') = go ns' r
in (Node l' n r', n+1 : ns'')
And through the magic of laziness, this all functions as weād want . This is one example of tying the knot. I like it because itās elegant (look how it mirrors the df solution function! ), not trivial, and at the same time not so hard that it becomes completely impossible to understand (for me).
TLDR: Laziness is cool.
[0] Iām certain most āadvancedā haskellers will be aware of the concept, though perhaps not the specific example.
[1] There are actually multiple solutions for the lhs of the function. It can be done using a queue or by splitting the tree into a list of lists, where each list contains the values of a specific ālevelā, and then constructing the correct tree back with that list. The one student that managed to fill it in did it using a queue approach; the other TA besides me did it with the list of list approach.