Modules
Haskell organises code into modules, each contained in a single source file. A module exports a set of names — values, types, type classes, and constructors — that other modules may import. Names not in the export list are private to the module. The mechanism is structurally similar to a Java module or a C# namespace, with the additional features of qualified imports (import qualified Data.Map as M), selective imports (import Data.List (sort, nub)), and re-exports (a module exposing names from imported modules). Beyond modules, Haskell uses lexical scoping: every binding has a scope determined by where it is introduced, with let, where, function arguments, and do-bindings as the principal local-scope mechanisms.
Module declaration
Every Haskell source file may begin with a module header:
module Data.Geometry
( Point(..)
, Shape(..)
, distance
, area
) where
import Data.List (foldl')
import qualified Data.Map.Strict as Map
-- ... declarations
The principal elements:
module Data.Geometry— the module’s name. Conventionally matches the file path:Data.Geometrylives inData/Geometry.hs.(...)— the export list. Names listed here are exported; names not listed are private.where— separates the header from the declarations.
If the export list is omitted, all top-level names are exported. The conventional discipline in modern Haskell is to write an explicit export list.
The module name is hierarchical: dots in the name correspond to directories. The standard library uses Data.List, Data.Map, Control.Monad, System.IO, and so on; the conventions admit a tree-like organisation.
The export list
The export list controls which names are visible to importers:
module Data.Geometry
( -- * Types
Point
, Shape(..) -- export Shape and all its constructors
, Triangle(Triangle, sides) -- selective export of constructors
-- * Functions
, distance
, area
, translate
-- * Re-exports
, module Data.Number
) where
The notation:
Point— exports the typePointonly (constructors are private; users cannot constructPointvalues directly).Shape(..)— exportsShapeand all its constructors.Triangle(Triangle, sides)— exportsTriangleand selected constructors/fields.module Data.Number— re-exports all names from the importedData.Numbermodule.
Comments may use Haddock section markers (-- * Types) to group exports for documentation generation.
The abstract data type pattern uses the export list to hide constructors:
module Data.Counter
( Counter
, newCounter
, increment
, value
) where
data Counter = Counter Int -- constructor not exported
newCounter :: Counter
newCounter = Counter 0
increment :: Counter -> Counter
increment (Counter n) = Counter (n + 1)
value :: Counter -> Int
value (Counter n) = n
Users cannot construct a Counter directly with Counter 5 — only through newCounter, which guarantees the invariant. The mechanism is the conventional Haskell encapsulation pattern.
Imports
The import directive brings names from another module into scope:
import Data.List -- import everything
import Data.List (sort, nub) -- selective: only sort and nub
import Data.List hiding (sort) -- everything except sort
import qualified Data.Map as Map -- qualified: must use Map.foo
import qualified Data.Map.Strict as Map -- the strict variant
import Data.Map (Map) -- import the Map type unqualified
-- but the operations qualified
The forms:
| Form | Effect |
|---|---|
import M | Bring all of M’s exports into scope unqualified |
import M (a, b) | Bring only a and b into scope |
import M hiding (a) | Bring everything except a |
import qualified M as N | Bring all of M’s exports into scope as N.x |
import qualified M | Bring as M.x (no alias) |
import qualified M as N (a, b) | Selective qualified import |
The conventional discipline:
- Use selective imports (
import M (a, b)) for clarity. - Use qualified imports for modules whose names would clash with others (
Data.Map,Data.Set). - Mix qualified imports with type-only unqualified imports:
import qualified Data.Map as Mplusimport Data.Map (Map)admitsMap k vas a type andM.lookup k mfor operations.
Multiple imports of the same module accumulate; importing Data.List (sort) and then Data.List (nub) makes both available.
Implicit imports: the Prelude
The Prelude module is implicitly imported into every Haskell file:
-- These are all available without explicit import:
length, map, filter, foldr, head, tail, take, drop
print, putStrLn, getLine
(+), (-), (*), (/), (++), (.), ($), (==), (<), (>)
True, False, Maybe, Just, Nothing, Either, Left, Right
IO, return, (>>=), (>>)
Functor, Applicative, Monad, Show, Eq, Ord, Num
The Prelude carries the conventional foundational vocabulary. To suppress the implicit import:
import Prelude ()
import qualified Prelude as P
The NoImplicitPrelude language extension produces the same effect for the file. The mechanism is conventional in code that wants to use a custom prelude (e.g., RIO, relude, protolude) instead of the standard one.
The four scopes
Haskell has four principal scopes:
Module scope
Top-level declarations are in module scope:
module Foo where
constant :: Int
constant = 42 -- module scope
helper :: Int -> Int
helper n = n + constant -- can see constant
main :: IO ()
main = print (helper 10) -- can see helper and constant
Module-scope bindings are visible throughout the module. The export list controls which are visible to importers.
Function (parameter) scope
Function parameters are in scope within the function body:
greet :: String -> String
greet name = "Hello, " ++ name -- name is in scope here
Parameters shadow same-named module-scope bindings within the function.
let scope
A let binding introduces names visible in the in expression (or in the enclosing do-block):
average :: [Double] -> Double
average xs =
let total = sum xs
count = length xs
in total / fromIntegral count
The total and count bindings are in scope only within the in expression. let bindings can refer to each other (the bindings are mutually recursive).
where scope
A where clause attaches to a top-level definition or guard, with bindings visible throughout the right-hand side:
quadratic :: Double -> Double -> Double -> Double -> Double
quadratic a b c x = result
where
result = a * x * x + b * x + c
classify :: Int -> String
classify n
| n > 0 = positive
| n < 0 = negative
| otherwise = zero
where
positive = "positive"
negative = "negative"
zero = "zero"
where bindings can refer to the function’s parameters and to each other. The conventional choice between let and where:
wherefor definitions that conceptually belong with the function but are too detailed to inline.letfor bindings local to a single expression.
The choice is largely stylistic; some codebases use where more, others use let.
Pattern bindings in do-notation
Within a do-block, bindings can be introduced with <- (for monadic actions) or let (for pure expressions):
main :: IO ()
main = do
line <- getLine -- bind the result of getLine
let upper = map toUpper line -- pure binding
len = length upper
putStrLn upper
print len
The <- binds the result of an IO action; the let introduces pure bindings. Both are scoped to the rest of the do-block.
Lambda parameter scope
A lambda introduces bindings visible in its body:
addOne :: Int -> Int
addOne = \x -> x + 1 -- x is in scope in `x + 1`
uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f = \(a, b) -> f a b -- a and b are in scope
Lambdas are first-class values; they may capture variables from the enclosing scope.
Shadowing
Inner bindings shadow outer bindings of the same name:
x :: Int
x = 1
f :: Int -> Int
f x = x + 1 -- the parameter x shadows the module-level x
main :: IO ()
main = do
print (f 10) -- prints 11
let x = 100 -- shadows module-level x within this block
print x -- prints 100
The compiler often warns about shadowing (-Wname-shadowing); the conventional discipline is to avoid shadowing where possible by choosing distinct names.
Type-variable scope
Type variables in a signature are scoped to the signature:
f :: a -> a -- a is in scope only in the signature
f x = x
g :: a -> a -- a here is NOT the same as a in f's signature
g x = x
The ScopedTypeVariables extension admits using the type variable in the body:
{-# LANGUAGE ScopedTypeVariables #-}
f :: forall a. a -> a
f x = (x :: a) -- a is in scope here
The extension is widely used in modern code; without it, type variables in signatures cannot be referenced in the function body.
Common patterns
Selective imports for clarity
import Data.List (sort, nub, group, foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
The pattern admits using Map k v as a type and Map.lookup, Map.insert for operations, while also importing specific list functions unqualified.
Hidden constructors for abstract types
module Data.Counter
( Counter
, newCounter
, increment
, value
) where
data Counter = Counter !Int -- constructor private
-- ... (constructors are not exported; only the operations)
Users construct Counter values only through the exposed functions.
Re-exporting from a façade module
module Data.Foo
( module Data.Foo.Internal
, module Data.Foo.Util
, Specific(..)
) where
import Data.Foo.Internal
import Data.Foo.Util
import qualified Data.Foo.Specific as Specific
The pattern admits a single import for users while the internal organisation uses several modules.
Internal modules
Many libraries expose an Internal namespace for modules that are technically public but not part of the stable API:
Data.Map -- the public API
Data.Map.Internal -- internals; subject to breakage between versions
Users importing the Internal module accept that it may change without notice.
A note on circular imports
Haskell admits module A importing module B and module B importing module A, but only with care. The conventional disciplines:
- Use hs-boot files (
.hs-boot) to declare the interface of a module without its implementation, breaking the circularity. - Restructure the modules to remove the cycle (typically by extracting shared types into a third module).
The conventional advice is to avoid circular imports where possible; when they are unavoidable, the .hs-boot mechanism is the standard solution.
Common patterns
qualified for clarity
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
main = do
let m = Map.fromList [("a", 1), ("b", 2)]
let s = Set.fromList [1, 2, 3]
let t = T.pack "hello"
...
The qualified form makes the source of each operation explicit; the cost is the per-call qualification.
Custom prelude
{-# LANGUAGE NoImplicitPrelude #-}
import RIO -- a custom prelude
The RIO and relude packages provide alternative preludes that exclude partial functions, prefer Text over String, and otherwise enforce a more rigorous coding style than the standard Prelude.
Pattern-matching imports
The PatternSynonyms extension admits importing pattern synonyms — abstract patterns that match against constructors without exposing them:
module Data.Foo
( Foo
, pattern Empty
, pattern Singleton
) where
pattern Empty :: Foo
pattern Empty <- (isEmpty -> True)
pattern Singleton :: a -> Foo a
pattern Singleton x <- (toSingleton -> Just x)
The mechanism is rare in routine code but useful for libraries that want to admit pattern-matching against an abstract type.