This article shows how to solve the “circular imports” problem in Haskell, using the C preprocessor, in a swift way. If you are in a hurry, go to C preprocessor section and do not skip Caveats.
The difficulty is well known:
you start a new Haskell project. After a cup of coffee, with glee and hope you flesh your first data-types and functions:
data Big = Big
data Test = Test Big
testFun :: Big -> Test
testFun = undefined
bigFun :: Test -> IO Big
bigFun = undefined
after a while the initial module is getting bigger and bigger, so you decide it would be a good idea to split it in manageable pieces:
-- A.hs
module A where
data Big = Big
bigFun :: Test -> IO Big
bigFun = undefined
-- B.hs
module B where
data Test = Test deriving (Show)
testFun :: Big -> Test
testFun = undefined
you eagerly run cabal new-build
, but GHC complains:
Module imports form a cycle:
module ‘B’ (src/B.hs)
imports ‘A’ (src/A.hs)
which imports ‘B’ (src/B.hs)
“Lord, why me?!”
There are three accepted ways of dealing with circular imports:
putting every datatype in a big Types.hs module: this is quick, but now your data is away from your functions, making any edit action a chore. “Types-sink” modules will grow themselves very big, very fast!
using type parameters (as explained in the HaskellWiki): my gripe with type parameters is that with nested structures you might end up having top types with a lot of parameters.
Moreover adding parameters when the structure is not parametric makes reading the code a chore. Which one would you rather go through, this:
data Game r q m b = Game r q m b
or this?
data Game = Game Room Quarterstaff Magic Book
using hs-boot (explained in the GHC manual): hs-boot
files are
like “signature files”, you put in there the minimal information to allow
a successful compilation, e.g.:
-- B.hs-boot
module B where
data Test
Importing B
like this
import {-# SOURCE #-} B
will solve the import problem.
hs-boot
forces you to modify two files for each change to the
code; deciding where to “break the cycle” can become a not-so-trivial
decision when using it on an already big codebase.
Check this repository to see a cabalised example.
What I am proposing here is a quick hack that uses the C preprocessor
(cpp) to break circular imports. Download the working example and let
us see what it is all about. We will start from file A.hs
:
module A where
#define beta
#include "B.hs"
data Big = Big
bigFun :: Test -> IO Big
bigFun = undefined
We do not import B
via import B
, but using #include
. Note the
#define beta
before it. Examining B.hs
will reveal the trick:
#ifdef beta
-- data declaration
data Test = Test Big
#undef beta
#elif 1
-- your module
module B where
import A
someFun :: Big -> Test
someFun = undefined
#endif
Data declaration is sandwiched between #ifdef beta
and #undef beta
,
while the rest of the module goes between #elif 1
and #endif
.
It is not difficult to see the preprocessor executes two passes: one to
transfer data-types to A.hs
(via CPP), the second to allow the rest of
B.hs
to be compiled by GHC. The benefits of this approach are:
Types.hs
sink-module with everything in it.data Test
, the compiler will point at file B.hs
,
line 3.B.hs
is an actual Haskell file, so you are not losing syntax-highlight.I advise to add a ghc-options: -XCPP
in your .cabal file, otherwise you
are forced to put a {#- Language CPP -#}
on top of every module.
I tested this solution in real life, these are the warnings:
import module A
, writing meaningful
qualified imports on types gets difficult (thanks to Massimo
Zaniboni for pointing this to me).makeLenses
and similar after your data imports/declarations in
A.hs
.:set -DSTAGE=2
to your local .ghci config file.cpphs
, as it is slightly better behaved
than plain CPP in corner cases (e.g. multiline strings).If you need a quick fix to mutually recursive modules in Haskell, using CPP is an option that can save you headaches. Hopefully the problem will soon be addressed at compiler level.