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
= 42 foo
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
= 42 foo
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 namee
and a qualified nameM.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.