Boilerplate Removal with Uniplate

by Neil Mitchell

Generic transformations and queries are often referred to as boilerplate code - they remain relatively similar as the action performed by the code changes, and can often outnumber the actual intent of the code in terms of lines. While other generic traversal schemes have shown how powerful new features can be added to compilers, and how the type system can be manipulated into accepting these operations, the Uniplate library focuses on a conceptually simpler generic concept. A more complete document on Uniplate was published at the Haskell Workshop 2007, and is available from the project website, along with a video presentation, and the associated thesis chapter.

Uniplate is a simple, concise and fast generics library. To expand on that sentence:

  1. A generics library is one which allows you to write functions that operate over a data structure without tying down all aspects of the data structure. In particular, when writing an operation, you don't need to give a case for each constructor, and you don't have to state which fields are recursive.
  2. Uniplate is the simplest generics library. Using Uniplate is within the reach of all Haskell programmers.
  3. Uniplate is more concise than any other generics library.
  4. Uniplate is fast, not always the absolute fastest, but massively faster than many generics libraries.
  5. Uniplate is also less powerful than some other generics libraries, but if it does the job, you should use it.

The Uniplate library can be installed with the standard sequence of cabal commands:

cabal update
cabal install uniplate

This document proceeds as follows:

  1. Using Uniplate
  2. Using Biplate
  3. Making Uniplate Faster



Using Uniplate

To demonstrate the facilities of Uniplate, we use a simple arithmetic type:

{-# LANGUAGE DerivingDataTypeable #-}
module Expr where
import Data.Generics.Uniplate.Data

data Expr = Val Int
          | Add Expr Expr
          | Sub Expr Expr
          | Div Expr Expr
          | Mul Expr Expr
          | Neg Expr
          deriving (Show, Eq, Data, Typeable)

In this definition, the Uniplate specific bits are bolded. The three extra parts are:

This definition makes use of the Scrap Your Boilerplate (SYB) based Uniplate implementation. The SYB implementation is compatible with the other implementations, but is slower (between 2 and 8 times) and requires some modest compiler extensions (implemented in GHC for many years). The alternative definition scheme is described towards the end of this document, in "Making Uniplate Faster". I recommend using the SYB implementation to start with, as it requires least work to use.

The Uniplate library defines two classes, Uniplate and Biplate, along with a number of functions. After importing Data.Generics.Uniplate.Data all types which have Data instances automatically have the necessary Uniplate instances. In the following subsections we introduce the Uniplate functions, along with examples of using them. The two most commonly used functions are universe (used for queries) and transform (used for transformations).

Finding the constant values


When manipulating our little language it may be useful to know which constants have been used. This can be done with the following code:

constants :: Expr -> [Int]
constants x = nub [y | Val y <- universe x]

Here the only Uniplate method being used is universe, which when given a tree returns all the root of the tree, and all it's subtrees at all levels. This can be used to quickly flatten a tree structure into a list, for quick analysis via list comprehensions, as is done above.

Exercise: Write a function to test if an expression performs a division by the literal zero.

Basic optimisation



optimise :: Expr -> Expr
optimise = transform f
          f x = x

Here the Uniplate method being used is transform, which applies the given function to all the children of an expression, before applying it to the parent. This function can be thought of as bottom-up traversal of the data structure. The optimise code merely pattern matches on the negation of a literal, and replaces it with the literal.

Now lets add another optimisation into the same pass, just before the f x = x line insert:

    f (Add x y) | x == y = Mul x (Val 2)



Depth of an expression



depth :: Expr -> Int



Renumbering literals



uniqueLits :: Expr -> Expr
        f (Val i) = do
            y:ys <- get
            put ys
            return (Val y)
        f x = return x



Generating mutants



mutate :: Expr -> [Expr]
                  | (Val i, gen) <- contexts x]



Fixed point optimisation


When slotting many transformations together, often one optimisation will enable another. For example, the the optimisation to reduce.


Do something different in the odd and even cases. Particularly useful if you have free variables and are passing state downwards.

Monadic Variants

descendM :: Monad m => (on -> m on) -> on -> m on                         -- descend
rewriteM :: (Monad m, Uniplate on) => (on -> m (Maybe on)) -> on -> m on  -- rewrite

All the transformations have both monadic and non-monadic versions.

Single Depth Varaints

children :: Uniplate on => on -> [on]           -- universe
descend :: (on -> on) -> on -> on               -- transform
holes :: Uniplate on => on -> [(on, on -> on)]  -- contexts

Lot's of functions which operate over the entire tree also operate over just one level. Usually you want to use the multiple level version, but when needing more explicit control the others are handy.


Don't use Uniplate! The reasons are that there is little boilerplate, you have to handle every case separately. For example in our language we can write:

Using Biplate

All the operations defined in Uniplate have a corresponding Biplate instance. Typically the operations are just the same as Uniplate, with Bi on the end.


The biggest difference is for the functions childrenBi and descendBi. In these cases, if the starting type and the target type are the same, then the input value will be returned. For example:

childrenBi (Add (Val 1) (Val 2)) == [Add (Val 1) (Val 2)]
children (Add (Val 1) (Val 2)) == [Val 1, Val 2]

For example, you should never hvae descendBi in an inner recursive loop.

Making Uniplate Faster

To make Uniplate faster import Data.Generics.Uniplate.Direct, and provide Uniplate instances by generating them with the Derive tool.