Syntax
The syntax of Haskell is designed for terseness and for direct expression of mathematical notation. The grammar is small, the expression language is the foundation, and statements (in the C-family sense) do not exist — every construct is an expression that yields a value. The language uses layout (whitespace-sensitive blocks, called the off-side rule) instead of explicit braces and semicolons, though both forms are admissible. The style favours pure expressions, function composition, and pattern matching; reading non-trivial Haskell code requires fluency with the layout rule, the implicit currying, and the substantial set of operator-formed identifiers.
This page covers the surface a working programmer encounters routinely. The dedicated pages cover the major sub-grammars (the type system, type classes, pattern matching, monads).
A complete program
The classical hello world:
main :: IO ()
main = putStrLn "Hello, world!"
A more substantial example with imports and multiple definitions:
module Main where
import Data.List (sort)
import System.Environment (getArgs)
greet :: String -> String
greet name = "Hello, " ++ name ++ "."
main :: IO ()
main = do
args <- getArgs
let names = if null args then ["world"] else sort args
mapM_ (putStrLn . greet) names
The build and execution with the standard toolchain:
ghc Main.hs && ./Main alice bob
The :: IO () signature on main declares that main is an IO action that produces no useful result. The do-notation sequences IO operations; <- binds the result of an action to a name; let introduces a non-IO binding within the block.
Source character set
Haskell source is interpreted as Unicode (UTF-8 by default, with implementation-defined alternatives). The basic source set includes the ASCII letters, digits, and the conventional punctuation; identifiers may use any Unicode letter or digit characters. The compiler recognises ASCII, UTF-8 with BOM, and LANGUAGE pragmas (treated below) that may modify the behaviour.
Identifiers
Haskell distinguishes two identifier categories by their first character:
- Variable identifiers start with a lowercase letter or underscore:
x,count,myFunction,_temp. - Constructor identifiers start with an uppercase letter:
True,Just,MyType,IO.
The case distinction is significant. Variables are values, parameters, and (lower-case) functions; constructors are data constructors, type names, and type-class names. The two cannot be mixed: True is a constructor, true would be a variable, and they refer to different things.
Identifiers may include digits, underscores, and primes (') after the first character: x', myFunc2, state_n.
Operators
Operators are identifiers consisting entirely of symbol characters: +, -, *, ++, <>, >>=, <$>, >>>. Operators are first-class — they are functions named with symbols rather than letters. A function name in backticks (`div`, `mod`, `elem`) is used as an infix operator; an operator name in parentheses ((+), (<>)) is used as a prefix function:
sum_a = 1 + 2 -- + as an infix operator
sum_b = (+) 1 2 -- + as a prefix function
div_a = 10 `div` 3 -- div as an infix operator
div_b = div 10 3 -- div as a prefix function (the conventional form)
The duality is one of the language’s distinguishing features; it admits compact higher-order programming.
Comments
Two comment forms:
-- a line comment, terminated by the end of the line
{-
a block comment, possibly spanning multiple lines.
Block comments may nest:
{- nested comment -}
-}
Haddock comments (the documentation form) use a | after -- for “this comment documents the following declaration” or ^ for “this comment documents the preceding”:
-- | The principal greeting function.
greet :: String -> String
greet name = "Hello, " ++ name ++ "."
data Status
= Idle -- ^ The system is idle
| Running -- ^ The system is processing
| Stopped -- ^ The system has stopped
Haddock generates HTML documentation from these comments; the format is the conventional Haskell documentation system.
The layout rule
Haskell’s layout rule (also called the off-side rule) admits whitespace-sensitive blocks: a block opens with a column-aligned series of declarations, each starting at the same column. The rule replaces explicit braces and semicolons in most code:
main = do
putStrLn "starting"
x <- readLn
let y = x * 2
z = y + 1
print (y, z)
putStrLn "done"
The block starts after do (or where, let, of); each subsequent line at the same column is a member of the block; a line at a deeper column is a continuation; a line at a shallower column closes the block.
The explicit form with braces and semicolons is admissible but rare in idiomatic code:
main = do { putStrLn "starting"; x <- readLn; print x; putStrLn "done" }
The layout-based form is the conventional contemporary style. Mixed-indentation code can produce confusing parse errors; modern editors with Haskell support handle the alignment automatically.
Declarations
The principal forms:
-- Type signature (optional but conventional)
greet :: String -> String
-- Function definition
greet name = "Hello, " ++ name ++ "."
-- Multiple equations with patterns
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- Guards
classify :: Int -> String
classify n
| n > 0 = "positive"
| n < 0 = "negative"
| otherwise = "zero"
-- Local definitions: let
average :: [Double] -> Double
average xs =
let total = sum xs
count = length xs
in total / fromIntegral count
-- Local definitions: where
quadratic :: Double -> Double -> Double -> Double -> Double
quadratic a b c x = result
where
result = a * x * x + b * x + c
The let … in … form is an expression: the bindings are in scope in the in-expression. The where-clause attaches to a top-level definition or a guard: the bindings are in scope in the right-hand side.
Type signatures are optional — the compiler infers types — but the conventional discipline is to write them for top-level functions. Inferred types may be more general than expected; explicit signatures pin down the intended type.
Statements vs expressions
Haskell does not have statements in the C-family sense. Every construct is an expression that yields a value. The principal expression forms:
-- Conditional
if cond then a else b
-- Pattern matching
case expr of
pattern1 -> result1
pattern2 -> result2
_ -> default
-- Let binding (an expression)
let x = 1; y = 2 in x + y
-- Lambda
\x y -> x + y
-- Application
f x y -- Haskell application, no parentheses
-- Operator section
(+ 1) -- partially-applied (+)
(1 +) -- the other side
(`div` 2) -- partially-applied div
Even do-notation, which superficially resembles statements, is sequence of expressions tied together by the monad’s >>= operator.
Type qualifiers and modifiers
Haskell does not have C-family type qualifiers (const, volatile). Variables in Haskell are immutable by definition: a binding is an alias for an expression, not a memory location, and reassignment is impossible.
Several keywords modify declarations:
| Keyword | Effect |
|---|---|
data | Algebraic data type declaration |
newtype | Single-constructor zero-cost wrapper |
type | Type synonym (alias) |
class | Type class declaration |
instance | Type class instance |
deriving | Auto-derive class instances |
module | Module header |
import | Module import |
where | Local bindings |
let / in | Local bindings (expression form) |
case / of | Pattern matching |
if / then / else | Conditional |
do | Monadic sequencing |
The keyword set is small; the language’s expressiveness comes from the combinators built on top.
The LANGUAGE pragma
Haskell extends the base language with a substantial set of optional features, each enabled by a LANGUAGE pragma at the top of a source file:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
module Main where
-- ...
Common extensions:
| Extension | Purpose |
|---|---|
OverloadedStrings | String literals can have any type with a IsString instance |
TypeApplications | Explicit type-application syntax @T |
ScopedTypeVariables | Type variables in signatures are in scope in the body |
BangPatterns | Strictness annotations on patterns |
LambdaCase | \case instead of \x -> case x of ... |
DeriveFunctor, DeriveFoldable | Auto-derive Functor, Foldable |
DerivingStrategies, DerivingVia | Control the deriving mechanism |
TupleSections | (,5) as \x -> (x, 5) |
MultiParamTypeClasses | Type classes with more than one parameter |
FunctionalDependencies | Functional dependencies in type classes |
GADTs | Generalised algebraic data types |
TypeFamilies | Type-level functions |
RankNTypes | Higher-rank polymorphism |
Many extensions are widely used and considered de facto standard in modern Haskell. The conventional pragma block at the top of a file may carry a dozen or more extensions.
The Haskell2010 standard is the latest formal language standard; modern code typically uses Haskell2010 plus selected extensions. The committee work on GHC2021 (the contemporary curated set of extensions) has consolidated the conventional set.
A note on the absence of statements
Several features the C-family takes for granted are absent from Haskell:
- Mutable variables — variables in Haskell are immutable bindings. Stateful computation uses the
IORef,STRef, orMVartypes in dedicated monads. - Loops — no
for,while, ordo … while. Iteration is expressed through recursion or higher-order functions likemap,foldr,replicateM. - Statements — every construct is an expression.
- Implicit conversions — Haskell has no implicit numeric conversions (no
inttodouble); explicit conversions throughfromIntegral,realToFrac,toRationalare required. nullreferences — Haskell has no nullable references; theMaybetype makes absence explicit.- Subtyping — Haskell has no class hierarchy; type classes admit ad-hoc polymorphism through constraints.
- Operator overloading by class — operators are functions; they are not associated with classes.
The combination — pure expressions, immutable bindings, recursion-based iteration, explicit effect-typing, no nulls, no subtyping — is the substance of what makes Haskell distinctive. The discipline of writing Haskell is the discipline of expressing computation through these primitives.