May 31, 2021
Written by Artyom Kazak
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 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:
makeLenses ''Tree
) starts a new "declaration group".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.
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 https://blog.monadfix.com/th-groups
$(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.