Surprises of the Haskell module system (part 1)

Published on

While the basics of the Haskell module system are easy to understand, there are certainly interesting corner cases and non-obvious (mis)features.

All of these are documented in the Haskell report, but the tricky details may be hard to notice. Another good read on this subject is A Formal Specification for the Haskell 98 Module System by Iavor S. Diatchki, Mark P. Jones, and Thomas Hallgren.

Empty module header

What happens when the export list is omitted, like in the following example?

module M where

foo = 42

This one is easy: all the locally defined types, classes and values are exported.

We might assume that something similar happens when we omit the module header altogether, like if we put

foo = 42

in a file by itself.

Contrary to this intuition, and according to the standard, when you omit the module header, it is assumed to be module Main(main) where.

Sometimes you want to check whether a piece of Haskell code compiles. You may put it into a file and try ghc test.hs. But even if the code itself doesn’t contain errors, you’ll get a message

test.hs:1:1: The function `main' is not defined in module `Main'

The message may seem confusing at first: what’s the module Main it’s talking about, and why do I need this main function? Well, now you know the answer.

Module re-exports

You probably know that you can re-export a whole module from your own module. The syntax is

module M (module N) where
...

The semantics of this re-export is a bit tricky, though. For example, what can we say about the following module?

module M (module Data.List) where
import qualified Data.List

You’d probably think that module M now exports everything that is exported by Data.List. Not at all! The standard says (emphasis is mine):

The form module M names the set of all entities that are in scope with both an unqualified name e and a qualified name M.e.

Since we imported Data.List qualified, nothing from it is in scope under an unqualified name. So, M doesn’t export anything. Right? Not quite.

The last thing we need to observe here is that although the single module imported by M explicitly is Data.List, there’s always an implicit import — Prelude. And these two share quite a lot in common — functions like foldr, map, length etc. All these satisfy the criterion above — they are in scope with both unqualified and qualified names, although these names come from different modules.

To summarize, the module M defined above will export exactly the intersection of Prelude and Data.List, plus instances exported by either of these modules.