Polyglot
Languages Haskell syntax
Haskell § syntax

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:

KeywordEffect
dataAlgebraic data type declaration
newtypeSingle-constructor zero-cost wrapper
typeType synonym (alias)
classType class declaration
instanceType class instance
derivingAuto-derive class instances
moduleModule header
importModule import
whereLocal bindings
let / inLocal bindings (expression form)
case / ofPattern matching
if / then / elseConditional
doMonadic 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:

ExtensionPurpose
OverloadedStringsString literals can have any type with a IsString instance
TypeApplicationsExplicit type-application syntax @T
ScopedTypeVariablesType variables in signatures are in scope in the body
BangPatternsStrictness annotations on patterns
LambdaCase\case instead of \x -> case x of ...
DeriveFunctor, DeriveFoldableAuto-derive Functor, Foldable
DerivingStrategies, DerivingViaControl the deriving mechanism
TupleSections(,5) as \x -> (x, 5)
MultiParamTypeClassesType classes with more than one parameter
FunctionalDependenciesFunctional dependencies in type classes
GADTsGeneralised algebraic data types
TypeFamiliesType-level functions
RankNTypesHigher-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, or MVar types in dedicated monads.
  • Loops — no for, while, or do … while. Iteration is expressed through recursion or higher-order functions like map, foldr, replicateM.
  • Statements — every construct is an expression.
  • Implicit conversions — Haskell has no implicit numeric conversions (no int to double); explicit conversions through fromIntegral, realToFrac, toRational are required.
  • null references — Haskell has no nullable references; the Maybe type 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.