So this Zipper problem from Exercism.io got me a little busy! Aside from being very useful, it also served as a mind puzzle for quite a few days.
Can’t say why really, but I just couldn’t figure out how to model this problem. Since I haven’t been familiar with the concept before, I started by reading the Haskell document. And it was really fun and easy to understand, and the Greek mythology sketches where totally hilarious! But, when it came to actually implementing it, I kind of got stuck for a while. Maybe it had to do with how all those mathematical explanations and reading existing implementations in Haskell or Clojure or even Elixir mixed up and created much confusion in my head.
Then I took Gregory‘s advice and let it simmer for a week, without actually doing anything about it, and voilà, solved it today! Talking about Hammock Driven Development 😀
For the uninitiated, the zipper is a fancy data structure that allows you to go up and down a tree in a cheap and easy to track manner. Changes are allowed as well. And it is not limited to trees either!
So anyway, this will be more of an overview of specific points and why some of them created the confusion in the first place (and some still do).
To begin with, this particular Exercism.io problem tries to build a zipper for a binary tree structure:
@type bt :: %BT{
value: any,
left: BinTree.t | nil,
right: BinTree.t | nil
}
The initial question was how to actually structure the zipper record? By most definitions, a zipper record contains:
- node’s value
- left branch
- right branch
- trail
A trail might be something like:
@type trail :: :top
| { :left, BT.t, trail }
| { :right, BT.t, trail }
So trail is a recursive structure that keeps records of the traversal path. The current node’s trail is it’s context and it is customary to call that node the focus. I guess the focus means that you don’t actually change the tree, but rather the point at which you are viewing the tree structure from. Could be viewpoint as well 🙂 The Haskell document has a nice analogy here:
“Well, indeed you don’t need a thread. Another view is to literally grab the tree at the focus with your finger and lift it up in the air. The focus will be at the top and all other branches of the tree hang down. You only have to assign the resulting tree a suitable algebraic data type, most likely that of the zipper.” – Ariadne
My first point of confusion was the combination of list of zipper record properties and what I had in my mind after reading the descriptions. The Exercism.io hint mentioned all the properties and I thought about something like:
@type zipper :: {
left: BT.t,
right: BT.t,
value: any,
trail: trail
}
After loosing some sleep on it, it finally occured to me that I already have many of the properties inside the BT structure! So, the final zipper looks like:
@type zipper :: { BT.t, trail }
Simple, not easy 😀
So now we have the zipper record that contains the node (BT.t) and the trail that says how we got to that node. Now we can do stuff like create zipper from a tree:
def from_tree(bt) do
{bt, :top}
end
Or go left:
def left({%{left: left}, _}) when is_nil(left) do
nil
end
def left({bt, trail}) do
{bt.left, {:left, bt, trail}}
end
We can’t go left from the leftmost node, otherwise we create a new zipper record, containing the left node as the new focus and adding current focus to the trail. Similar solution goes for going right. An important thing to notice here, and the second thing that confused me, is to not think of “going left” as finding a sibling on the left, but rather finding the first child on the left branch. Funny how wrong interpretation of a simple term causes havoc!
Returning to some previous node is thusly considered as “going up”. It essentially means we need to go back in history, or rather follow our trail back one step:
def up({_, :top}) do
nil
end
def up({_, {_, parent, trail}}) do
{parent, trail}
end
We can’t go up from the top node, and for the rest we just return the parent and it’s trail as the new focus or point of view into the tree.
So now we can traverse the tree in any direction and this also gives us the ability to get the tree (from the root node) from any other node:
def to_tree({bt, :top}) do
bt
end
def to_tree(zipper) do
zipper |> up |> to_tree
end
As you can see, the traversal is rather cheap.
And now something completely different! We also need to be able to change the tree:
- by changing the focus node’s value
- by replacing focus node’s left branch
- by replacing focus node’s right branch
This is the point that got me confused the most. Namely, the hints and docs mention that the zipper record contains node’s value. So my initial attempts were are related to changing that part of the zipper, be it change of node’s value or one of it branches. This is how the trail portion looked at that time:
@type trail :: :top
| { :left, any, BT.t, trail }
| { :right, any, BT.t, trail }
Note the “any” part that represents the value. That is what is actually missing from the final solution. It occurred to me that since we have BT.t inside the zipper record, it would be just fine to update it’s value / structure directly. Here’s a first attempt:
def set_value({focus, trail}, new_value) do
{focus | %{value: new_value}, value, trail}
end
This works nicely when you inspect the current focus. You get node’s new value, or it can even be applied to structure (replacing left or right branch) and it works from the perspective of the focus node. But, the problem arises when we try to fetch the resulting tree. Then, the focus is changed until we reach the top, and the top node doesn’t know about the change we made on the original focus node. So, then you get the original tree rather then the updated one.
I tried to figure out if that hinted “value” in zipper record could be used to save the day, but alas, no luck in that department. Then, after “deliberate rest period”, it dawned on me that the trail contains everything we need to make this work:
def set_value({focus, {direction, parent, trail}}, new_value) do
focus = focus | %{value: new_value}
{_, parent} = parent |> Map.get_and_update(direction, fn(focus_in_parent) ->
{focus_in_parent, focus}
end)
{focus, {direction, parent, trail}}
end
So what is going on here? First we calculate the new value for the focus, that hasn’t changed. But, then we go to parent in the trail, and update it’s representation of the focus node to contain the updated value as well. Here the Map.get_and_update/3 greatly helps, since it allows us to fetch the focus node and update it at the same time. Note that we actually do know how to get the current focus in parent since we recorded it’s position during traversal. E.g. on going left, the trail recorded that :left, and we can use it now to get to our focus node in parent structure.
Finally, with a bit of refactoring to make the above function applicable to all updates, we can do something like:
def set_value(zipper, value) do
update(zipper, value, :value)
end
def set_left(zipper, left) do
update(zipper, left, :left)
end
def set_right(zipper, right) do
update(zipper, right, :right)
end
And that’s it, we have a functional zipper for our binary tree structure! We can now do stuff like:
bt |> Zipper.from_tree |> Zipper.left |> Zipper.set_left(bt(6, leaf(7), leaf(8))) |> Zipper.to_tree
bt |> Zipper.from_tree |> Zipper.left |> Zipper.set_value(100) |> Zipper.to_tree
As always you can find the complete solution at Github, but feel free to inspect some other Exercism.io solutions as well:
All the Zipper solutions @ Exercism.io
Norbert Melzer’s solution
Kevin Smith’s solution
Zach Kemp’s solution
Zipper resources:
You could have invented zippers
Haskell/Zippers + Theseus and the Zipper Story
Gérard Huet. The Zipper. Journal of Functional Programming
Getting Acquainted With Clojure Zippers
Clojure Zippers: Structure Editing With Your Mind
Clojure Zipper
Roll Your Own Window Manager: Tracking Focus with a Zipper
ZipperTree – Elixir
Erlang port of Clojure’s Zipper by Inaka