Anatomy of a Haskell file
Haskell is a standardized, general-purpose purely functional programming language, with non-strict semantics and strong static typing.
File extensions: .hs
Every part of the example below is labelled and explained. This page is one of 55 annotated tours on AnatomyOf, a free, open-source project by LunarWerx Studios.
What is inside a Haskell file
Module declaration
Defines the module name and optionally exported functions.
Every Haskell file belongs to a module, declared with module Name (exports) where at the top of the file. The parenthesized export list controls what other modules can see; anything left out stays private to the module, which is Haskell's main tool for encapsulation. Omitting the export list (module Main where) exports everything defined in the file. By convention the module name mirrors the file path, so Data.Map lives in Data/Map.hs. The compiler enforces this for anything beyond a simple Main module.
Import statement
Brings in functions and types from other modules.
import Data.List (sort) pulls specific names into scope, while a bare import Data.List brings in everything the module exports. qualified imports, as in import qualified Data.Map as Map, require every use to be prefixed (Map.lookup), which avoids name clashes between modules that export similarly-named functions. Haskell resolves imports at compile time, and the standard library (base) plus packages from Hackage are typically managed through Cabal or Stack rather than being bundled with the compiler.
Comment
Single-line (--) or block ({- ... -}), ignored by compiler.
A line comment starts with -- and runs to the end of the line. Block comments are delimited with {- and -}, can span multiple lines, and (unlike C-style block comments) nest properly, so commenting out a chunk of code that already contains a block comment is safe. Haddock, Haskell's documentation tool, repurposes comment syntax with a leading | or ^ (e.g. -- | Describes this function) to generate API documentation directly from source comments.
Type alias
Creates a new name for an existing type (type ...).
type Radius = Double introduces a synonym: Radius and Double are completely interchangeable to the compiler and to type inference. Aliases exist purely to make signatures more readable and self-documenting, they carry no runtime cost and no extra type safety. Because a type alias is not a distinct type, it cannot be used to prevent mixing up a Radius and a plain Double by accident. When that distinction matters, Haskell offers newtype, which wraps a value in a genuinely new type that is erased at compile time but still checked separately.
Algebraic data type (ADT)
Defines a composite data type (data ...).
data Shape = Circle Radius | Rectangle Double Double declares a type with two constructors: a value of type Shape is either a Circle holding one Radius, or a Rectangle holding two Doubles. The | separates alternative constructors, making Shape a sum type, while the fields carried by each constructor make it a product type, hence "algebraic." ADTs are the backbone of data modeling in Haskell. Combined with pattern matching, they let the compiler check exhaustiveness: if a function forgets to handle one of Shape's constructors, GHC can warn about it at compile time, before the program ever runs.
Type signature
Specifies the types of a function's arguments and result (::).
calculateArea :: Shape -> Double reads as "calculateArea is a function from Shape to Double." Signatures are optional (Hindley-Milner type inference can determine most types on its own) but they are considered essential style: a signature documents intent and turns a whole class of mistakes into compile errors instead of runtime surprises. Because Haskell is purely functional, a signature also tells you a lot about what a function can and cannot do. A function typed Int -> Int cannot perform IO or throw a checked exception; any side effect has to show up in the type, usually via IO somewhere i
Function definition & pattern matching
Defines function behavior based on input patterns.
Haskell functions are often defined as a series of equations, one per pattern: calculateArea (Circle r) = ... matches only when the argument was built with the Circle constructor, binding r to its field. calculateArea (Rectangle w h) matches the other constructor and binds both fields in one step, replacing the manual field access other languages need. Pattern matching works on any data structure, not just custom ADTs: matching directly on lists, tuples, and literal values is common. Clauses are tried top to bottom, and GHC's exhaustiveness checking can flag patterns that were never covered.
Guard
Boolean condition (|) to select a function definition clause.
A guard attaches a boolean condition to a pattern-matched clause: | w > 0 && h > 0 = w * h only fires when both the pattern matches and the condition holds. Guards are tried in order, and the catch-all otherwise (simply defined as True) is the idiomatic way to write a default case. Guards read like a cleaned-up if/else-if chain but stay inside the equational style of the rest of the function, and they can appear alongside where bindings that are shared across all of a clause's guards.
List comprehension
Concise syntax for creating lists ([ expression | pattern <- list ]).
[calculateArea s | s <- shapes] reads as "the list of calculateArea s for each s drawn from shapes." The part after | can include multiple generators and boolean filters, e.g. [x * y | x <- xs, y <- ys, x /= y], giving a compact notation borrowed directly from set-builder notation in mathematics. List comprehensions are syntactic sugar over map, filter, and concatMap: the compiler desugars them into ordinary function calls, so there is no performance difference, only a difference in how readable the intent is at the call site.
Main function
The entry point for an executable, with type IO ().
main :: IO () is the program's entry point; the () ("unit") return type means main is run for its effects, not its result. Because Haskell is purely functional, all interaction with the outside world (printing, reading input, file access) is threaded through the IO type, which marks a computation as impure right in its signature. The do block is syntactic sugar for chaining IO actions (and other monadic values) sequentially. let inside a do block binds a local name without needing in, and each subsequent line runs after the previous action completes.
Official Haskell site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.