Declaration groups: where order of declarations suddenly matters in Haskell

May 31, 2021
Written by Artyom Kazak

The problem

Let's say you have a few types that reference each other:

data Tree a = Empty | Node { value :: a, branches :: Forest a }
type Forest a = [Tree a]

Now you want to generate lenses for Tree, so you do this:

data Tree a = Empty | Node { value :: a, branches :: Forest a }
makeLenses ''Tree

type Forest a = [Tree a]

And suddenly you are hit with this seemingly unrelated error:

example.hs:5:54: error:
    Not in scope: type constructor or class ‘Forest’
5 | data Tree a = Empty | Node { value :: a, branches :: Forest a }
  |                                                      ^^^^^^

What gives?

The explanation

The GHC manual has the explanation in the Template Haskell section, but I can't even link directly to it, so I will paste the relevant quote here:

Top-level declaration splices break up a source file into declaration groups. A declaration group is the group of declarations created by a top-level declaration splice, plus those following it, down to but not including the next top-level declaration splice. N.B. only top-level splices delimit declaration groups, not expression splices. The first declaration group in a module includes all top-level definitions down to but not including the first top-level declaration splice.

Each declaration group is mutually recursive only within the group. Declaration groups can refer to definitions within previous groups, but not later ones.

In other words:

  • Every top-level splice (like makeLenses ''Tree) starts a new "declaration group".
  • Declarations can refer to other declarations from the current and previous declaration groups, but not the follow-up groups.
  • For curious souls: the relevant bit of GHC source is tc_rn_src_decls.

Let's see how the example above will be split into groups:

-- GROUP 1
data Tree a = Empty | Node { value :: a, branches :: Forest a }

-- GROUP 2 (every top-level splice starts a new group)
makeLenses ''Tree
type Forest a = [Tree a]

Tree is in group 1 and Forest is in group 2, so Forest can refer to Tree but not the other way round.

The easiest solution is to put all Template Haskell splices at the end of the file. Just have a section with makeLenses, deriveJSON and so on.

The problem, but the other way around

Counterintuitively, sometimes you want to create declaration groups on purpose.

For instance, let's say you have a non-top-level splice:

data Foo = ...
instance ToJSON Foo where
  toJSON = $(mkToJSON defaultOptions ''Foo)

Since this is all a single declaration group, $(mkToJSON ...) can't actually refer to Foo! Yes, I said "you can refer to everything in the current and previous groups", but this does not include splices. Splices can only see declarations in the previous groups, but not in the current one.

The solution is to break the group manually:

data Foo = ...

-- See
$(pure [])

instance ToJSON Foo where
  toJSON = $(mkToJSON defaultOptions ''Foo)

This is super non-obvious. If you are a library designer, you can catch this case with recover and try to help the user, like we do in safe-wild-cards.