
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? thanks!