100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Pure Functions and Immutability

Understand what makes a Haskell function pure, why referential transparency matters, and how immutability changes the way you update data and reason about programs.

Core ConceptsBeginner8 min readJul 10, 2026
Analogies

What Makes a Function Pure

A pure function in Haskell is one whose output depends only on its input arguments and which produces no observable side effects — it doesn't write to a file, mutate a global variable, print to the console, or depend on hidden state like the system clock. Call square x = x * x a thousand times with x = 4 and you get 16 every single time, with nothing else in the program changed as a result. This property is the default in Haskell: the language has no ordinary assignment statement that lets a function silently alter a variable somewhere else, so unless a function's type signature mentions IO, you can trust it is pure just by reading its signature.

🏏

Cricket analogy: A pure function behaves like a fixed net-practice drill: feed the bowling machine the same seam angle and speed every time, and Sachin Tendulkar's cover drive response is identical, unaffected by the scoreboard or the crowd noise outside.

Referential Transparency

Referential transparency means any expression can be replaced by its evaluated value without changing the meaning of the program — if addTax price = price * 1.18 is pure, then every occurrence of addTax 100 in your code can be swapped for 118.0 and the program behaves identically. This lets you reason about Haskell code algebraically, the way you'd simplify a math expression, because there's no need to trace through mutable state to know what a call will do. Compilers exploit this too: GHC can freely reorder, share, or memoize pure computations because it knows evaluating addTax 100 twice can never produce two different answers.

🏏

Cricket analogy: Referential transparency is like knowing a Duckworth-Lewis-Stern par score for a rain-affected match will always compute to the same target given the same overs and wickets lost, so commentators can substitute the number anywhere without recalculating.

haskell
-- Pure: same input always gives same output, no side effects
square :: Int -> Int
square x = x * x

-- Referentially transparent: this call can be replaced by 16 anywhere
result :: Int
result = square 4 + square 4  -- equals 16 + 16, always

-- Impure territory (for contrast): actions in IO depend on the outside world
-- getLine :: IO String  -- NOT pure: depends on external input each run

Immutability and Its Payoffs

In Haskell, once a value is bound to a name, it cannot be changed — let x = 5 doesn't create a mutable box you can later overwrite; it names the value 5 for that scope, and any 'update' actually produces a brand-new value. Updating a list or record, such as newPerson = oldPerson { age = 31 }, builds a fresh record sharing unchanged fields with the old one rather than mutating oldPerson in place, so every other part of the program still sees the original, untouched oldPerson. This eliminates an entire class of bugs where one function accidentally corrupts data that another function was relying on, and it also makes concurrent code safer because immutable data can be freely shared across threads without locks.

🏏

Cricket analogy: It's like a scorecard that gets photocopied for a substitution: when a player retires hurt and a substitute fielder is used, a new scorecard entry is created rather than erasing the original figures already recorded.

In Haskell, IO actions like putStrLn or readFile are still values — calling putStrLn "hi" doesn't print anything by itself, it just builds an IO () action. Only when that action is connected to main and actually run by the runtime does the side effect happen, which is how Haskell keeps purity even while supporting real-world I/O.

A common misconception is that 'pure' means 'has no IO type in scope' — but code can still be effectively impure if it depends on unsafe escape hatches like unsafePerformIO. Avoid unsafePerformIO in ordinary code: it lies to the type system about purity and can break the compiler's optimizations and your reasoning about referential transparency.

  • A pure function's output depends only on its arguments, with zero observable side effects.
  • Referential transparency lets you substitute a pure expression with its value anywhere in the code.
  • Haskell has no mutable assignment; 'updating' a value always creates a new value instead.
  • Record update syntax like r { field = x } builds a new record sharing unchanged fields.
  • Purity and immutability make equational reasoning, testing, and refactoring dramatically simpler.
  • Immutable data can be shared across threads safely without locks, aiding concurrency.
  • IO actions are ordinary values in Haskell; effects only happen when run via main.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#PureFunctionsAndImmutability#Pure#Functions#Immutability#Makes#StudyNotes#SkillVeris#ExamPrep