Introduction

I like using Brent Yorgey’s diagrams1 package to create images by writing Haskell2. Here’s a small example:

[]

and the code to generate it:

hello :: Diagram B
hello = vsep 5 
        . zipWith letterRow [ "HELLO", "WORLD!" ] 
        $ L.tails cols
  where letterRow  ls  = centerX . hsep 5 . zipWith letterDisc ls
        letterDisc l c = letter l <> circle 10 # lw 2.5 # fc c
        cols     = cycle [ red, green, blue ]
        letter c = stroke (textSVG [c] 20) 
		         # fc yellow # lw 1.0 # lc yellow

In the example above graphics primitives circle, stroke, and textSVG are combined to make the final image. The combinators include hsep and vsep, which take lists of elements and stack them separated by a space, and the <> operator which puts one diagram on top of another.

There are lots of little modifiers e.g. lw which sets the line width: these are just functions, but the # operator lets us write the object being styled before the styling.

This article covers a few points which struck me as being particularly interesting, or I wanted to think about more carefully. All the information is included in the fine, official documentation:

A principled package

Like Haskell the diagrams package has a strong theoretical underpinning. As an example, an important distinction is made between a location in space (a Point) and a displacement in space (a Vector). Although both can be represented by a coordinate tuple, they are very different animals:

Some software conflates Points and Vectors, perhaps because they often have the same representation: the Haskell diagrams package doesn’t. If you think the distinction is worthwhile, then I think you’ll enjoy using diagrams; on the other hand, if you think it’s just pedantry I suspect you’ll be frustrated.

The official diagrams documentation has a helpful introduction to vectors and points6 which discusses all this in more detail.

Silly games with Vectors and Points

The basic type of a two-dimensional vector is V2 n where n tells us the underlying scalar type e.g. Double. You can make such a vector in lots of ways:

*Main> V2 1.0 2.0
V2 1.0 2.0
*Main> r2 (3.0, 4.0)
V2 3.0 4.0
*Main> (5.0 ^& 6.0) :: V2 Double
V2 5.0 6.0

You can make Points in similar ways, though note that there’s no P2 constructor:

*Main> p2 (1.0, 2.0)
P (V2 1.0 2.0)
*Main> (3.0 ^& 4.0) :: P2 Double
P (V2 3.0 4.0)

Although it’s an internal detail, we make a Point by wrapping a Vector. For example we could have written the last example above as:

*Main> (3.0 ^& 4.0) :: Point V2 Double
P (V2 3.0 4.0)

Having created Vectors and Points, we can now transform them. Here we translate a Vector and a Point, noting that the former is unchanged:

*Main> translateX 10 $ r2 (0,1)
V2 0 1
*Main> translateX 10 $ p2 (0,1)
P (V2 10 1)

As you might expect, we can do all this in three-dimensions too e.g.:

> (0.0 ^& 1.0 ^& 2.0) :: V3 Double
V3 0.0 1.0 2.0
*Main> translateX 10 $ p3 (0,1,2)
P (V3 10 1 2)

Polymorphism

The astute reader will have noticed that we applied translateX to both Points and Vectors, in both two- and three-dimensions. Clearly it’s a polymorphic function so let’s look at its type:

*Main> :t translateX
translateX
  :: (Additive (V t), Num (N t), R1 (V t), Transformable t) =>
     N t -> t -> t

This rather scary signature needs a bit of unpicking. Ignoring the stuff before the fat arrow, we have the type:

N t -> t -> t

Having seen how it’s used, we know that t is something like a Point or a Vector, and N t is a scalar of the appropriate type. In the examples above, we had e.g.:

t   ~> V2 Double
N t ~> Double

So it’s clear that N is a type level function which extracts the underlying type from a more complicated thing. Looking now at the constraints before the fat arrow, we also see V t which is the vector-space in which t lives.

Most of the constraints on t are straight-forward: it needs to be transformable, the underlying type has to be numeric, and so on. The most interesting term is R1 (V t) which loosely means that the vector-space in which t lives has to have a first dimension: R1 extracts that coordinate.

By contrast if we look at translateZ,

*Mail> :t translateZ
translateZ
  :: (Additive (V t), Num (N t), R3 (V t), Transformable t) =>
     N t -> t -> t

the R1 constraint is now R3 which constrains the vector-space to have a third-dimension. In practical terms this means that if we try to translate a two-dimensional point in the Z-direction, it will fail at compile time:

*Main> translateZ 10 $ p2 (0,1)

<interactive>:66:1: error:
    • Could not deduce (R3 V2) arising from a use of ‘translateZ’
      from the context: Num n
        bound by the inferred type of it :: Num n => P2 n
        at <interactive>:66:1-24
    • In the expression: translateZ 10
      In the expression: translateZ 10 $ p2 (0, 1)
      In an equation for ‘it’: it = translateZ 10 $ p2 (0, 1)

The meaning of this might not be immediately obvious to the casual observer.

Type classes

It’s worth stating explicitly that many of the functions in the diagrams API don’t take a particular type: rather they take any type which conforms to the relevant type class constraints. This is elegant and powerful, but it can lead to unwieldy signatures and Byzantine error messages. The User Manual has some useful tips and tricks7 on this topic.

More positively, if we return to the Transformable type class above, we can find many instances8. Unsurprisingly you can apply translateX to all sorts of things, including diagrams and other transformations. It’s nice that one function can move so many things.

As with the translation examples above, particular transformations may place other contraints on the objects which are being transformed. However any type will work if it has the necessary instances to satisfy the constraints.

The diagrams manual has a good Type class reference9 which explains all this and more.

Monoids

A general theme in Haskell is that abstract mathematical ideas are often translated into a Haskell type class. If you create something which obeys the laws of the type class, you can make an instance of the type class which both saves writing code and unifies syntax.

For example, a monoid10 is a structure with a single associative operation and an identity element. Essentially this means that we can take two things and combine them into another thing of the same type, and that there’s a particular element which doesn’t change things when you combine with it. If there isn’t such an identity element you formally have a semigroup, not a monoid, but I’ll gloss over that distinction here.

The Haskell type class corresponding to a monoid is Data.Monoid11, and a while ago I wrote some notes12 about it. Rather than rehashing that theory, let’s just look at some examples to illustrate the general idea.

You can make a list monoid where the operation is concatenation, and the identity element the empty list:

[1,2,3] <> [4,5,6] = [1,2,3,4,5,6]

[1,2,3] <> []      = [1,2,3]

[] <> [1,2,3]      = [1,2,3]

It’s easy to see that this is associative:

([1,2] <> [3,4]) <> [5,6] = [1,2,3,4] <> [5,6]

                          = [1,2,3,4,5,6]

[1,2] <> ([3,4] <> [5,6]) = [1,2] <> [3,4,5,6]

                          = [1,2,3,4,5,6]

We could also make a monoid from the integers under addition with zero as the identity (or a different one under multiplication):

1 <> 2 = 3

1 <> 0 = 1

0 <> 1 = 1

(1 <> 2) <> 3 = 3 <> 3

              = 6

1 <> (2 <> 3) = 1 <> 5

              = 6

Although the meaning of the <> operator changes, it obeys the same rules in both cases.

Similarly, we can make a monoid for diagrams. Here, the operator means putting one diagram on top of the other:

[]

I think it’s clear that the empty diagram is a perfectly good identity element here.

Turning back to the operator, the order matters, as it does with lists. Mathematically, we’d say that the operator isn’t commutative:

[]

However, the operator is associative and that’s all that matters if you want to be a monoid:

[]

[]

More diagrammatic Monoids

Besides diagrams themselves, the diagram package has many other monoid instances.

For example, if you have two transformations you can either apply them sequentially, or combine them into one uber-transformation and then apply that. So we can make a monoid instance for transformations.

Other examples abound: the word ‘Monoid’ appears nearly fifty times in the documentation for Diagrams.Core13.

Making a # of things

Diagrams makes extensive use of # which is flipped function application. fc red is a function which makes the foreground-colour of a diagram red. You might use it thus:

	fc red (circle 2)

but it is more elegant to say:

	circle 2 # fc red

It’s worth emphasizing that there’s nothing diagrams specific about #. You could also say things like:

    > "Wibble" # length
    6

The & function in Data.Function is similar, but has a lower precedence (1 vs 8). If we used this instead, we would typically need more parentheses, and we’re writing Haskell not Lisp.

Named diagrams

Diagrams (including subdiagrams) can be named, and then subsequently referred to by name. This is extremely helpful because it allows you to refer to some element of a diagram after it’s been composed.

For me it greatly extended the scope of the diagrams I could make without using explicit coordinates14. )

There are many ways to use names, but for simple cases I use named15 to give something a name:

circle 1 # fc green # named "Foo"

and withName16 to operate on a named subdiagram:

addMark n = withName n $ 
                atop . place (circle 0.1 # fc red) . location

Names don't have to be strings: you can use any instance of isName17.

Chess board example

To show how useful names can be, consider the example below which draws a chess board with named squares, then fills it with pieces.

[]

data Square = Square Char Int
  deriving (Eq, Ord, Show)
instance IsName Square

chessBoard :: PreparedFont Double -> Diagram B
chessBoard f = L.foldl' (flip draw) cbBoard piecePosns
  where draw (n,s) = withName n $ 
                        atop . place (myText f 16 [s]) . location

piecePosns :: [(Square, Char)]
piecePosns = concatMap (uncurry doFile) [('a', "♖♘♗♕♔♗♘♖")
                                        ,('b', "♙♙♙♙♙♙♙♙")
                                        ,('g', "♟♟♟♟♟♟♟♟")
                                        ,('h', "♜♞♝♛♚♝♞♜")
                                        ]
  where doFile file = zipWith (\r p -> (Square file r, p)) [1..8]

cbBoard :: Diagram B
cbBoard = vcat . reverse 
               . zipWith cbRank ['a' .. 'h' ] 
               $ L.tails bgs 
  where cbRank rank        = hcat . zipWith (cbCell rank) [1..8]
        cbCell rank file b = square 10.0 
                               # fc b # lw 0.5 # lc black 
                               # named (Square rank file)
        bgs = cycle [darkgoldenrod,lightgoldenrodyellow]

myText :: PreparedFont Double -> Double -> String -> Diagram B
myText f h t = stroke (textSVG' opts t) 
                  # fc black # lw 0		
	where opts = TextOpts f INSIDE_H KERN False h h

Font Acknowledgment

I should begin by saying that I’m using Alexander Lange’s fine Quivira font18 to draw all the pieces, which makes things much easier. If you want to use this:

Implementation

The key function is cbBoard which draws an empty board by assembling squares into ranks, then ranks into the board. The cells are all named with their rank and file e.g. Square 'c' 7. It is nice that you can use almost anything sensible as a name with relatively little effort.

Having generated the board, we just fold over a list of pieces and their locations, grab the cell by its name and draw the piece on it. At no stage do we have to worry about where the cell is: we just ask for it by name.

Conclusions

In many ways I think the diagrams package is a microcosm of Haskell itself: there’s quite a steep learning curve, because it embraces some clever and abstract ideas. However, once you’ve absorbed those ideas it’s a joy to use and affords new insights into the problem you’re trying to solve.