Roman Cheplyaka

Surprises of the Haskell module system (part 1)

December 25, 2012

While basics of the Haskell module system are easy to understand, there are certainly interesting corner cases and non-obvious (mis)features. In this and the following articles we will explore some curious examples.

All of these are documented in the Haskell report, but the tricky details are hard to notice and recognize (although the reader is encouraged to read the report nevertheless!). 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 presume that something similar happens when we omit the module header entirely — like if we just 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 some Haskell snippet compile. You may put it into a file and try ghc test.hs. But even if the snippet 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, above is 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 export 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.

Subscribe to future articles via RSS