A language tour
A function has no side effects. A value never changes. The type system is a proof assistant. Correctness by construction.
01 — Pure Functions
In Haskell, a function cannot read from a file, mutate a variable, or print to the screen — unless the type signature says so. This purity isn't a restriction: it's a guarantee. Pure functions are trivially testable, trivially composable, and trivially parallelisable.
"In Haskell, the purity of functions is enforced by the type system. A function that has effects must advertise those effects in its type — the type is a contract, and the compiler is the enforcement."
— Simon Peyton Jones, principal designer of GHC-- Pure function: same inputs → always same output fibonacci :: Int -> Int fibonacci 0 = 0 fibonacci 1 = 1 fibonacci n = fibonacci (n-1) + fibonacci (n-2) -- Effectful function: IO in the type signature greet :: String -> IO () greet name = putStrLn ("Hello, " ++ name ++ "!") -- main lives in IO — it's the only place effects are allowed to escape main :: IO () main = do putStrLn (show (map fibonacci [0..10])) greet "Haskell"
The type IO () is not a special keyword — it's an ordinary type. IO is a monad that sequences effects. The purity/effect boundary is tracked in the type system, not by convention.
02 — Type Inference
Haskell has one of the most powerful type systems in any mainstream language — and you rarely need to write type annotations, because the compiler infers them. When you do write types, they serve as machine-checked documentation.
-- Type signatures are documentation — not required but prized square :: Num a => a -> a square x = x * x -- Works for Int, Integer, Double, Float — any Num instance square 5 -- 25 :: Int square 3.14 -- 9.8596 :: Double -- Algebraic data types — model your domain precisely data Shape = Circle Double | Rectangle Double Double | Triangle Double Double Double area :: Shape -> Double area (Circle r) = pi * r * r area (Rectangle w h) = w * h area (Triangle b h _) = 0.5 * b * h
Pattern matching on ADTs is exhaustive — the compiler warns if you forgot a case. Adding a new constructor to Shape causes every function that pattern-matches on it to warn until you handle the new case.
03 — Lazy Evaluation
Haskell is lazy by default — expressions are evaluated only when their value is needed. This enables infinite data structures, separation of generation from consumption, and elegant definitions that would diverge in a strict language.
-- Infinite list of all natural numbers — evaluates lazily naturals :: [Integer] naturals = [1..] -- take forces only the first 10 elements take 10 naturals -- [1,2,3,4,5,6,7,8,9,10] -- Infinite list of fibonacci numbers fibs :: [Integer] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) -- The 50th fibonacci number — computed on demand fibs !! 50 -- 12586269025 -- Sieve of Eratosthenes — elegantly lazy primes :: [Integer] primes = sieve [2..] where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs) defines fibonacci as a self-referential lazy stream. It's one of the most cited examples of lazy evaluation's expressive power.
04 — Typeclasses
Typeclasses are Haskell's mechanism for ad-hoc polymorphism — defining interfaces that any type can implement. They're the inspiration for Rust's traits, Swift's protocols, and Java's interfaces, but older and more powerful than all of them.
-- Define a typeclass class Describable a where describe :: a -> String data Color = Red | Green | Blue -- Implement for Color instance Describable Color where describe Red = "a warm red" describe Green = "a calm green" describe Blue = "a cool blue" -- Works for any Describable — the constraint is in the type printDescription :: Describable a => a -> IO () printDescription x = putStrLn ("This is " ++ describe x) -- Functor, Applicative, Monad are typeclasses too -- fmap (+1) [1,2,3] == [2,3,4] -- List is a Functor -- fmap (+1) (Just 5) == Just 6 -- Maybe is a Functor
fmap has the type (a -> b) -> f a -> f b — it works for any Functor f. The same function maps over lists, Maybe, Either, trees, and any other container you define.
05 — Function Composition
In Haskell, function composition is written with a dot: f . g means "apply g, then apply f." Programs are built by composing small functions into larger ones. Point-free style eliminates explicit arguments, expressing transformations as pipelines.
import Data.Char (toUpper, isAlpha) import Data.List (nub, sort) -- Compose functions with (.) — right to left shout :: String -> String shout = map toUpper . filter isAlpha -- Point-free: no explicit argument needed sortedUnique :: Ord a => [a] -> [a] sortedUnique = sort . nub -- deduplicate, then sort -- ($) applies a function — useful for removing parentheses main :: IO () main = do putStrLn $ shout "hello, world!" -- HELLOWORLD print $ sortedUnique [3,1,4,1,5,9,2,6] -- [1,2,3,4,5,6,9]
Point-free style is not just aesthetic — it encourages thinking of functions as transformations to be composed, not procedures to be executed. The argument is just the data flowing through the pipeline.
06 — The Whole Picture
More PL research has been done in or inspired by Haskell than any other language. Its influence runs through the industry.
Monads, type classes, GADTs, lazy evaluation — all pioneered in Haskell, all later adopted by mainstream languages.
If it compiles, it often works. The type system encodes invariants that prevent entire categories of bug.
GHC is a world-class optimising compiler. Haskell programs can be competitive with C on the right workloads.
Standard Chartered, HSBC, and Jane Street use Haskell for financial modelling where correctness is non-negotiable.
Haskell is hard. That's part of the point — it expands how you think about types, effects, and program structure.