Title: How to Create a Doubly-Linked Dodecahedron
Author: Christian Sattler
Draft Version: Fri Apr 6 20:15:49 UTC 2012
Abstract:
This document was originally created in response to a challenge
from Richard Bird in his talk "How to Link a List" at FITA 2012.
He proposed a method for constructing not only a doubly-linked list,
but also a "doubly-linked" torus. Jesting, he ended his talk with
the challenge of constructing a doubly-linked dodecahedron.
Here, we take up his challenge seriously, presenting a general
elegant method for constructing doubly-linked structures of
symmetric objects based on two well-known notions,
memoization and (representations of) symmetry groups.
This method is sufficiently abstract to not only solve the above
specific challenge within the character limit of a Twitter message,
as Richard Bird joked in his talk, but also generalize to other
graph structures with nicely representable local automorphisms.
Executive Summary (Twitter message, 138 characters [with line breaks]):
import Data.Function.Memoize
data N=N[Integer]N N
c u=(.flip map u.(!!))
t f=memoFix(\s x->N(f x)(c[1,0,2,4,3]s x)$c[0,4,1,3,2]s x)[0..4]
Short Explanation:
One problem is that there is no canonical way of assigning names for
the three directions to the neighbours of each vertex in the
dodecahedron as exists for the torus (left, right, up, down). The
canonical solution is to include direction information in each node,
representing each vertex three times, yielding 60 nodes represented by
even permutations of size 5. Now, given a node n = N pi x y, let
address n = pi represent the "address" of n (with equality of
"addresses" implying "identity"), let swap n = x represent the next
node in the implicit direction, with the new direction pointing back
to the old vertex, and rot n = y represent the same vertex with the
implicit direction rotated by 120 degrees. It is clear that this
yields complete navigability of the dodecahedron. For example, we have
"identities" swap (swap n) = n and rot (rot (rot n)) = n. Less
trivially, we for example have an "identity" iter 5 (rot . swap) n = n.
Detailed Explanation:
Let us look at the type [Nat] = nu X. 1 + Nat x X where Nat = nu T. 1 + T.
Unfortunately, the difference between inductive and coinductive types is
not expressible in Haskell, so we make clear that we are interested only in
finite lists and natural numbers. Then, given any type A, there is a
canonical isomorphism between [Nat] -> A and the coinductive type mu Y. A x
Stream(Y) where Stream(Y) = mu Z. Y x Z. Let f and g be the mediating
arrows, i.e. f o g = id and g o f = id. Now, with lazy evaluation, even
though g o f is extensionally equal to the identity, it acts as a
"memoization" operator on [Nat] -> A, transforming any function into an
equivalent function which evaluates each function value only ever once.
Obviously, we can construct memoization operators for many more types, and
that is precicely what the Memoize library does for us. For example, let
Z/(m) denote the integers modulo m : Nat with the usual additive structure.
The torus from your talk can be seen as Z/(m) x Z/(n). Given a function f :
Z/(m) x Z/(n) -> A on it, we can create a doubly-linked structure from it
by defining
record TorusNode A where
element : A,
left : TorusNode A,
right : TorusNode A,
up : TorusNode A,
down : TorusNode A
g = memoize g' where g' z = TorusNode {
element = f z,
left = g (z + (-1, 0)),
right = g (z + ( 1, 0)),
up = g (z + ( 0, -1)),
down = g (z + ( 0, 1))
}
or simply
g = memoize (liftA5 TorusNode
f
(g . (+(-1, 0)))
(g . (+( 1, 0)))
(g . (+( 0, -1)))
(g . (+( 0, 1)))
)
where we exploited that X. Z/(m) x Z/(n) -> X is an applicative functor.
Now, with memoization, the problem of constructing a doubly-linked
structure on a given graph is not so much a problem of backlinking any more
but more a problem of how to represent the local structure of the graph
globally using appropriate automorphisms. For the torus, this was quite
easy because of the naturally given additive structure. For the regular
dodecahedron, it is helpful to know that the symmetry group of
orientation-preserving Euclidean transformations (i.e., rotations) is
isomorphic to A_5, the group of even permutations on five elements [0..4].
This isomorphism can be constructed as follows:
There is a canonical way (up to isomorphism, up to renumbering there are
two choices) to color the vertices of a dodecahedron with five colors
[0..4] such that no two adjacent vertices have the same color. One sees
easily that this coloring must also have the property that each face is
incident to exactly one vertex of each color. A less trivial insight is
that, given a face and an indident starting vertex, each even permutation
appears exactly once as the permutation corresponding to the list of colors
of the vertices of the face, counting in counter-clockwise direction. Given
an orientation-preserving Euclidean transformation, we can now identify
this transformation with the permutation of colors it induces one the
vertices of some fixed face (or, inversely, the permutation of colors it
induces between the vertices of some fixed face and the face it is mapped
to). An amazing fact of naturality is that this permutation does not depend
on the choice of the face. This permutation is always even, so this
correspondence induces an isomorphism between the group of
orientation-preserving symmetries and A_5.
[Note that the full symmetry group of the regular dodecahedron is not
isomorphic to S_5, but a different semidirect product A_5 with Z/(2). The
reason for this is that counting the colors of the vertices of a face in
clockwise direction instead still does not yield an odd permutation,
because it corresponds to composition of the counter-clockwise permutation
with (\x -> 4 - x) = (04)(13), an even permutation. This is an artifact of
the number five and "the reason" the full symmetry group of the regular
tetrahedron is S_4, since (02) is an odd permutation.]
Now, let us try to define a node type for the dodecahedral graph.
Obviously, we cannot make use of directions like left/right/up/down for the
torus. Instead we invent something like
record DodecahedronNode A where
element A,
back : DodecahedronNode A,
left : DodecahedronNode A,
right : DodecahedronNode A
where each vertex comes with a direction 'back' indicating the edge we came
from. 'left' and 'right' then correspond to the other two edges, in the
obvious order. Effectively, we represent each vertex as three different
nodes, but this is necessary for this approach. By the above, each element
of pi : A_5 induces an orientation-preserving transformation of the
dodecahedron and hence a permutation pi^{(Node)} of nodes, but this is a
transitive action (in the sense of actions in group theory), so we can
break this symmetry by associating a specific arbitrary node x_id to the
identity element of A_5 and then associating pi : A_5 with x_pi :=
pi^{(Node)}(x_id).
Now, there is exactly one pi_back : A_5 such that x_{pi_back} is the node
we arrive at when choosing the 'back' link from x_id, i.e. x_{pi_back} =
back(x_id), and similar permutations pi_left, pi_right : A_5. Given any
other node x_pi with pi : A_5, pi^{(Node)} maps x_id to x_pi and hence also
should map
back (x_id) to back (x_pi),
left (x_id) to back (x_pi),
right(x_id) to right(x_pi)
because it corresponds to an orientation-preserving symmetry of the
dodecahedron. This means we must define
back(x_pi) = pi^{(Node)}(back(x_id))
= pi^{(Node)}(pi_back^{(Node)}(x_id))
= (pi o pi_back)^{(Node)}(x_id)
= x_{pi o pi_back}
and similarly for 'left' and 'right'. In other words, given a function f :
A_5 -> B for some arbitrary type B representing the data to be attached to
each node, we can create a "doubly-linked" dodecahedron out of it by
defining
g = memoize g' where g' pi = DodecahedronNode {
element = f pi,
back = g (pi . pi_back ),
left = g (pi . pi_left ),
right = g (pi . pi_right)
}
or simply
g = memoize (liftA4 DodecahedronNode
f
(g . (. pi_back ))
(g . (. pi_left ))
(g . (. pi_right))
)
in complete analogy to the torus, now that we have exploited the symmetry
of the dodecahedron. Trying to fit this into a twitter post, we arrive
import Data.Function.Memoize
data N a=N a(N a)(N a)
c u=(.flip map u.(!!))
t f=memoFix(\s x->N(f x)(c[1,0,2,4,3]s x)$c[0,4,1,3,2]s x)[0..4]
which is exactly 140 character and would work were it not for the obstacle
that the type of the permutations defaults to [Int] instead of [Integer]
and the Memoize library does not seem to work correctly on [Int] -> a at
the moment (resulting in the topmost version). The only major difference to
the above explanation is that we chose a different representation of
dodecahedral nodes using operators 'swap' and 'rot' and an (arbitrarily)
inverted semantics of direction, with the understanding that this still
yields complete navigability of the dodecahedron, for example by defining
back = conjugate_swap(swap) = swap,
left = conjugate_swap(rot) = swap . rot . swap,
right = conjugate_swap(rot . rot) = swap . rot . rot . swap
In particular, we have pi_swap = [1,0,2,4,3] (0 maps to 1, 1 to 0, 2 to 2,
3 to 4, 4 to 3), pi_rot = [0,4,1,3,2], map (v !!) u is composition v o u
(so c u s v = s (v o u)), x_id = t f, and the rest should fall into place.
It might be more elegant to represent permutations as Integer -> Integer
instead of [Integer] (because composition of permutations is then just
composition of functions), but the brevity of the list notation made the
latter the only choice for meeting the character limit.