Pure Functions
The smallest, most important idea in functional programming — and why it's the foundation of everything that follows
If you remember only one idea from this entire course, make it this one: a pure function is a function whose output depends only on its inputs, and which has no observable side effects.
That's it. Pure functions are the atom of functional programming. LINQ, immutability, composition, parallelism, testing, caching — they all get easier when the building blocks are pure.
A definition you can hold in your head
A function f is pure if and only if:
- Same input → same output. Given the same arguments,
falways returns the same value. Forever. Across machines, threads, and time of day. - No side effects.
fdoes not write to disk, print, mutate shared state, mutate its arguments, throw on the clock, send a network request, increment a global counter, read the time, or read from a random number generator.
The dashed arrows are what makes g impure: it reaches outside the
box.
Pure vs impure: side by side
Add and Doubled are pure. NowMs, Greet, and AppendOne are
not — and each is impure in a different way.
The four superpowers of pure functions
Pure functions are not just an aesthetic preference. They have concrete, measurable advantages.
1. They're trivially testable
A pure function's test is just Assert.Equal(expected, f(input)). There's no setup, no mocking, no teardown.
Contrast with testing an impure method that writes to a database: you have to spin up a fixture, manage the connection, clean up between tests, and reason about ordering. Pure tests are free.
2. They're safe to run in parallel
Two threads calling a pure function with the same input cannot
interfere — there's nothing shared to interfere with. This is why
PLINQ (parallel LINQ) works: the operators are assumed to be pure.
3. They're cacheable (memoizable)
f(x) always returns the same value, so you can compute it once and
remember it. This is called memoization.
You can only do this for pure functions. Memoizing an impure function changes its behavior.
4. They're easy to reason about
You can read a pure function top to bottom, and once you understand it, you understand it forever. It doesn't matter who calls it, when, in what order, or with what state. The function is a self-contained mathematical mapping.
Identifying impurity in real code
Most "impurity" in everyday code falls into a small number of categories. Learn to spot them:
| Pattern | Why it's impure |
|---|---|
Console.WriteLine(...) | Writes to stdout |
File.ReadAllText(...) | Reads from disk |
DateTime.Now / DateTime.UtcNow | Hidden input (the clock) |
Random.Next() | Hidden input (RNG state) |
httpClient.GetAsync(...) | Network I/O |
list.Add(x) (on a parameter) | Mutates input |
static int counter; counter++; | Mutates global |
throw new Exception(...) on some inputs | Subtle: makes "same input → same output" false |
The last one is interesting. A function that throws for some inputs is technically impure (its output is not a return value for those inputs), but in practice we tolerate it for argument validation. The ones above it are the ones that bite.
"But my program has to print things"
Of course. A program that does nothing but compute pure values is useless. Real programs read input, do I/O, and produce output. The functional answer is not "no side effects ever". It is:
Push side effects to the edges of the program. Keep the core pure.
This is sometimes called the functional core, imperative shell pattern.
In a LINQ pipeline, the operators (Where, Select, etc.) are pure.
The materialization at the end (ToList, foreach, write to file)
is the impure shell.
A multi-file challenge
You'll implement two small pure functions and a small impure "driver" that uses them. The functions are pure; the driver does I/O. This is the functional-core / imperative-shell pattern in miniature.
Implement MathCore.SumOfSquares(IEnumerable<int> xs) and
MathCore.MeanOfSquares(IEnumerable<int> xs) as pure functions
in MathCore.cs.
SumOfSquaresreturns the sum of the squares of its inputs.MeanOfSquaresreturns the average of the squares of its inputs, or0.0for an empty input.
Do not print, mutate the input, or read external state. The
Program.cs shell calls them and prints the results.
Which of these C# methods is pure?
static int RollDie() => new Random().Next(1, 7);
static int Triple(int x) => x * 3;
static int LogAndReturn(int x) { System.Console.WriteLine(x); return x; }
static void Add(System.Collections.Generic.List<int> xs, int x) => xs.Add(x);
Modern C# as a Functional Language
A version-by-version tour of the C# features that made functional-style code idiomatic, ending with the modern toolset you'll use for the rest of the course
Immutability
Why values that never change make programs simpler, safer, and easier to compose — and how modern C# expresses immutability ergonomically