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.
-- 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 runImmutability 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.
IOactions are ordinary values in Haskell; effects only happen when run viamain.
Practice what you learned
1. Which of the following best defines a pure function in Haskell?
2. What does referential transparency allow a programmer or compiler to do?
3. In Haskell, what happens when you write `newP = oldP { age = 31 }`?
4. Why can immutable data be shared across threads without locks?
5. What is a key risk of using `unsafePerformIO` in ordinary pure code?
Was this page helpful?
You May Also Like
Recursion in Haskell
Understand why recursion is the primary looping mechanism in Haskell, how base cases and recursive cases work, and how tail recursion and laziness affect performance.
Pattern Matching in Haskell
Learn how Haskell lets you destructure values directly in function definitions and case expressions to write clear, exhaustive branching logic.
Lists and List Comprehensions
Learn how Haskell's linked-list data type works and how list comprehensions let you build, filter, and transform lists with concise, declarative syntax.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics