Introduction to circular programming

Today we will look at a programming problem and a cool solution. I will assume basic Haskell knowledge. The problem is simple: “Given a binary tree with integers in its leaves, replace all the values in the leaves by the minimum of the tree.”

Lets  give it a look. The tree is defined as follows

data Tree = Leaf Int | Node Tree Tree
           deriving Show

The initial attempt would be to traverse the tree twice, one to evaluate the minimum and a different one to make the replacements. We can see this solution in the following snippet

transform :: Tree -> Tree
transform t = rep t (tmin t)
    where rep (Leaf n)     x = Leaf x
          rep (Node lt rt) x = Node (rep lt x) (rep rt x)
          tmin (Leaf n)      = n
          tmin (Node lt rt)  = (tmin lt) `min` (tmin rt)

We can run ghci and see that it works fine

*Main> transform (Node (Node (Leaf 6) (Leaf 4)) (Leaf 1))
Node (Node (Leaf 1) (Leaf 1)) (Leaf 1)

but, can we do this with only one traversal?

The perks of being lazy

Haskell is a lazy language, by this I mean that it evaluates expressions only at need and as little as possible. For example, we can define an infinite list (usually called stream in other languages) like this:

ones :: [Int]
ones = 1 : ones

If we try to print the list, the program will never stop. However, we can take as many elements as we want without any problem

*Main> take 1 ones 
[1]
*Main> take 5 ones 
[1,1,1,1,1]
*Main> take 10 ones 
[1,1,1,1,1,1,1,1,1,1]

Another cool thing we can do, is define the following function

trace :: ((a, b) -> (c, b)) -> a -> c
trace f i = o
       where (o,z) = f (i,z)

The trace function gives the function f parts of its own output as an argument! Note that this is not recursion, this is called circular programming. This is not magic, the idea is that z will act as a pointer to a result we are expecting. Lets try to understand this by defining a function to solve our problem. This function, will take a pair, it first component will be a tree and the second parameter should represent something that we expect, in this case the minimum of the tree (an Int). So, we need something with the following specification.

repmin (t,n) = (rep t n, tmin t)

This states that n will hold the minimum of the tree and we make the replacements in the tree using this variable. Expanding the definition we get the following function

repmin :: (Tree, Int) -> (Tree, Int)
repmin (Leaf n , m) = (Leaf m , n)
repmin (Node l r, n) = (Node l' r', ln `min` rn)
                  where (l',ln) = repmin (l,n)
                        (r',rn) = repmin (r,n)

and if you have believed in me so far, we get that our solution to our problem is

 
transform' :: Tree -> Tree
transform' = trace repmin

and… the moment of truth:

*Main> transform' (Node (Node (Leaf 6) (Leaf 4)) (Leaf 1))
Node (Node (Leaf 1) (Leaf 1)) (Leaf 1)

It worked!

Now, when can we use this idea? Basically, any time the function we give as a parameter to trace is not strict. By non strict, I mean that it doesn’t force the evaluation of its argument, (the components of the pair it takes as input). We will talk more about laziness and strictness on a different post. For now, I hope you enjoyed this short presentation, you can read more about circular programming here.

Leave a comment