jmtd → log → Generic Haskell
When I did the work described earlier in template haskell, I also explored generic programming in Haskell to solve a particular problem. StrIoT is a program generator: it outputs source code, which may depend upon other modules, which need to be imported via declarations at the top of the source code files.
The data structure that StrIoT manipulates contains information about what
modules are loaded to resolve the names that have been used in the input code,
so we can walk that structure to automatically derive an import list. The
generic programming tools I used for this are from
Scrap Your Boilerplate
(SYB), a module written to complement
a paper of the same name.
In this code snippet, everything
and mkQ
are from SYB:
extractNames :: Data a => a -> [Name]
extractNames = everything (++) (\a -> mkQ [] f a)
where f = (:[])
The input must be any type which implements typeclass Data
, as must all its
members (and their members etc.): This holds for the Template Haskell Exp
types. The output is a normal list of Name
s. The utility function has a
more specific type Name -> [Name]
. This is all that's needed to walk over
the heterogeneous data structures and do something specific (f
) when we
encounter a Name
.
Post-processing the Name
s to get a list of modules is simple
nub . catMaybes . map nameModule . concatMap extractNames
Unfortunately, there's a weird GHC behaviour relating to the module names
for some Prelude functions that makes the above less useful in practice.
For example, the Prelude function words :: String -> [String]
can normally
be used without an explicit import (since it's a Prelude function). However,
once round-tripped through a Name
, it becomes GHC.OldList.words
. Attempting
to import GHC.OldList
fails in some contexts, because it's a hidden module or
package. I've been meaning to investigate further and, if necessary, file a GHC
bug about this.
For this reason I've scrapped all the above and gone with a different plan. We
go back to requiring the user to specify their required import list explicitly.
We then walk over the Exp
data type prior to code generation and decanonicalize
all the Name
s. I also use generic programming/SYB to do this:
unQualifyNames :: Exp -> Exp
unQualifyNames = everywhere (\a -> mkT f a)
where f :: Name -> Name
f n = if n == '(.)
then mkName "."
else (mkName . last . splitOn "." . pprint) n
I've had to special-case composition (.
) since that code-point is also used
as the delimiter between package, module and function. Otherwise this looks
very similar to the earlier function, except using everywhere
and mkT
(make
transformation) instead of everything
and mkQ
(make query).
Comments
everything
but I'm pretty sure I will at some stage :-). BTW, SYB stands for "Scrap your boilerplate" (as far as I know).