Turns out recursive imports are easy

🎉I am writing these notes at Brick, a magical mystery no-bullshit publishing platform. Turns out writing goes much faster when I don't have to hit “Publish” or do git commit.

You can use it too — check it out at Brick.do.


Previously I thought recursive imports (via .hs-boot files) were cumbersome, hard, broken, required machinations with Cabal, etc.

I just used them in anger and it turns out they are super easy.


Let's say you have two modules that use each other.

Expressions can include patterns:

module AST.Expr where

import AST.Pat
    
data Expr 
    = ... 
    | ExprLet (Pat, Expr) Expr    -- let ... = ... in ...
    deriving (Eq, Ord, Show)

Patterns can include expressions:

module AST.Pat where

import AST.Expr

data Pat 
    = ... 
    | PatViewPattern Expr Pat     -- (... -> ...)
    deriving (Eq, Ord, Show)

And then you get the famous “module imports form a cycle” error. Oh shit.


.hs-boot files to the rescue!

Step one: add {-# SOURCE #-} to some of the recursive imports. Not necessarily all — just enough to break the cycles.

import {-# SOURCE #-} AST.Pat

Step two: provide just enough information in the corresponding .hs-boot file. Only the things you actually use. Type definitions are not necessary — only their names. Instance contents aren't necessary — only their headers.

-- AST/Pat.hs-boot

module AST.Pat where

data Pat

instance Eq Pat    -- 'deriving' doesn't work, you have to write it like this
instance Ord Pat
instance Show Pat

That's it. You're done. If you have more things you need to include — functions, classes, etc — read the documentation.