[Haskell-cafe] circular imports
qdunkan at gmail.com
Mon Sep 6 13:30:43 EDT 2010
Lately I've been spending more and more time trying to figure out how
to resolve circular import problems. I add some new data type and
suddenly someone has a new dependency and now the modules are
circular. The usual solution is to move the mutually dependent
definitions into the same module, but sometimes those threaten to drag
in a whole zoo of other dependencies, *all* of which would have to go
into the same module, which is already quite large anyway. Of course
this requires lots of thought and possibly refactoring and is a big
pain all around.
I feel like the circular imports problem is worse in haskell than
other languages. Maybe because there is a tendency to centralize all
state, since you need to define it along with your state monad. But
the state monad module must be one of the lower level ones, since all
modules that use it must import it. However, the tendency for bits of
typed data to migrate into the state means it's easy for it to
eventually want to import one of its importers. And the state monad
module gets larger and larger (the largest modules in my system are
those that define state monads: 1186 lines, 706 lines, 1156
lines---the rest tend to be 100--300 lines).
I haven't really had this problem in other languages. Maybe it's
because I just don't write very big programs in other languages, or
maybe because some other languages are dynamically typed and don't
make you import a module to use its types, or maybe because some other
languages support forward declaration (actually, ghc haskell does
support a form of forward declaration in hs-boot files).
I have a few techniques to get out:
- Replace Things with ThingIds which have no big dependencies, and can
then be looked up in a Map later. This replaces direct access with
lookup and thows some extra Maybes in there, which is not very nice.
- Cleverly use type variables to try to factor out the problematic
type. Then I can stitch the data structure back together at a higher
level with a type alias. This is sort of complicated and awkward.
- Move the declarations that must be moved to the low level module,
re-export them from the module that defines their (smart)
constructors, and pretend like they belong to that module. This works
well when it can work, but makes the code awkward to navigate and
doesn't let you hide their implementation unless you give up and move
the rest of the code in as well.
- Just use an hs-boot. The main problem I've noticed with this so far
is that you wind up with a lot of recompilation, since ghc always
seems to want to start with the boot files and then recompile the
loop. This makes ghci use a little more annoying. Actually,
sometimes :r simply reloads the changed module, but sometimes it wants
to start again at the hs-boots and recompiles a whole pile, I'm not
sure what makes the difference. Probably making the loop as small as
possible would help here.
Is this a problem others have noticed? Any other ideas or solutions?
More information about the Haskell-Cafe