Refactoring Uniplate

At the Haskell Workshop 2007Neil Mitchell presented an approach to generic traversals of data that does not need “Scary Types”. He restricted his Uniplate library compared to existing approaches and was able to give an implementation based on a simple Haskell type class.

This post rephrases Neil’s enlightening ideas and simplifies (a bit) the implementation and use of his library. In the following, I will explain the ideas in my own words. I have implemented a Haskell library Traversal.hs for generic traversals and will comment on similarities and differences after documenting my library by example.

Features of the Traversal library

The main feature of the library is to support to query and manipulate children of tree-structured values. With little help from the programmer, the library provides powerful and flexible queries and transformations of tree structured values. Powerful, because sophisticated functions are implemented in the library independently of a concrete datatype, so the programmer does not need to write such often tedious traversal code herself. Flexible, because the provided functions allow to focus on the interesting part of the data and use default behaviour for other parts, so the programmer can later refine her datatypes without having to adapt transformations that are not concerned with the change.

Finally, the approach is comnpletely lightweight. The library compiles with both hugs and ghc – the currently most widely used Haskell systems. It uses multi-parameter type classes to provide a bit more general interface. An implementation within Haskell98 is very useful already and can be obtained by specializing some type signatures in the presented library.

A central type class

In order to use the library for her own datatype, the programmer has to make it an instance of the type class Traversable:

class Traversable a b
where scrap :: a -> ([b],[b] -> a)

This type class specifies a single function scrap that selects a list of children from an arbitrary value and provides a means to replace the children of the given value. The main restriction of this approach is that all children must have the same type. Thanks to multi-parameter type-classes, the type of the children can differ from the type of the “mother”. Although the approach is not as general as other approaches to generic traversals it is general enough for many practical examples.

Assume we have defined types for names and expressions and define a type for bindings based on these:

Note, how we can reuse the instance Traversable Binds Exp to implement the Let-case for the scrap function. The library function

family :: Traversable a a => a -> [a]

computes the list of the given value, its children, those children and so on, if the given value is traversable and its type equals the type of its children. Applied to an expression, it yields the list of all subexpressions of the given expression. We can use this function, e.g., to compute the names of all variables that are used in an expression:

usedVars :: Exp -> [Name]
usedVars e = [ n | Var n ← family e ]

Note that this function mentions only a single constructor of the Exp datatype. If we want to add new kinds of expressions – for example, case expressions for pattern matching – only the instance declaration for Traversable has to be adapted – usedVars does not have to be changed.

A simple transformation

We cannot only query subexpressions but also transform them with user defined functions. Assume we want to eliminate simple let expressions. A let expression is simple, if the bound expressions do not use the introduced variables:

A monad for scoping

An occurrence of a variable is called free if the variable is not in scope, i.e., bound by a let or lambda expression, when it is referenced. We use the following monad with two auxiliary functions to model scoping:

The function extendScope is special because it takes a monadic action and runs it in a different scope. This is more natural than using a state monad because variables that are bound in one subexpression can occur freely in another.

The function freeVars can now be defined in terms of a monadic action that computes free variables:

The function freeVarsScoped has the structure of a generalized fold, i.e., it matches all alternatives of the Exp datatype, calls itself recursively on subexpressions, and recombines the results to yield the overall result.

This is a very frequent scheme – many transformations are defined like this. However, if we mention all constructors explicitely in the definition of freeVarsScoped, this results in a poorly extensible implementation. We would have to change freeVarsScoped whenever we change the definition of Exp.

As we are only interested in the Var, Lam and Let cases, we are only willing to change freeVarsScoped when one of these constructors changes. We don’t care about the others.

Of course, the Traversal library comes to the rescue. It provides a function

We only explicitely mention the constructors that we are interested in. All other cases are summarized by the local function descend that combines the results of the different recursive calls that are hidden in the implementation of fold.

Comparison with Neil’s Uniplate library

The main difference between the Traversal and the Uniplate library is that the former defines a single type class Traversable and the latter defines two type classes:

The functions family and childFamilies can be implemented as mutually recursive functions quite naturally:

family :: Traversable a a => a -> [a]
family a = a : childFamilies a
childFamilies :: (Traversable a b, Traversable b b) => a -> [b]
childFamilies = concatMap family . children

The function uniplate cannot be implemented in terms of uniplateBi because an instance Uniplate a does not imply Biplate a a.

Moreover, Neil seems to have invested much more thoughts in making his implementation efficient. I’m wondering whether his optimizations could be imitated within the presented single type-class design.

Another topic for future investigations is to derive instances of the class Traversable automatically like it is possible with Uniplate. Until now, I did not feel the need for implementing this for my own projects because the instances are not very difficult to write by hand.

Try it out!

I did not describe the whole interface of the library. So watch Traversal.hs for more information.