1. Introduction
Functional programming (FP) is a programming paradigm where the main building block is not an object or a procedure/method, but a function in the mathematical sense. In FP the focus is on describing "what to compute", not "how to compute it".
You've already met parts of FP when working with lambda expressions and LINQ. So what's the difference? Practically: OOP describes objects and their interactions, procedural programming — a sequence of steps, and FP — function composition, passing behavior as values, avoiding state mutation (immutability) and eliminating side effects.
Why bother with a new paradigm?
- Cleaner, more predictable, and more testable code.
- Easier to support multithreading ("no state — no problems").
- Conciseness and expressiveness (less code — fewer bugs).
- High-level, easily reusable abstractions.
Analogy
Imagine a restaurant got an order: "make an omelette". An imperative cook runs a list of instructions: take eggs, crack, whisk, fry. A functional cook says: res = omelet(eggs) — they operate with functions, abstracting away the kitchen's internal state (well, mostly).
In C# we can use both approaches. That makes the language very flexible and powerful — especially for real projects.
Key FP concepts
1. Higher-order functions
Functions can be passed as parameters, returned from other functions, and stored in variables. You've already done this with lambda expressions and delegates. In FP these "functions over functions" are the foundation.
2. Pure functions
A function is "pure" if its result depends only on its parameters and it doesn't change anything outside itself (no side effects). Two identical calls with the same arguments produce the same result.
3. Immutability
Data isn't mutated "in place": a new state is a new object. This makes reasoning about the program much easier and helps with multithreading.
4. No side effects
A function doesn't write to a file, doesn't change global variables, doesn't draw on the screen — it just returns a result. In real life side effects are inevitable, but they are usually isolated at the edges of the system.
5. Function composition
One function can be built from others, like building blocks. For example: filter positive numbers, take their squares, and sum them. Each operation is a separate function and they combine easily (Where → Select → Sum).
2. FP in C#: from theory to practice
C# is a multi-paradigm language: it supports OOP, procedural style, and a powerful functional style (with lambdas, delegates, extension methods and LINQ).
Let's examine using an example from our learning app
Imagine we're developing a program that works with lists of numbers and strings. Our task is to apply different operations to that data in a functional style.
Example 1: Using higher-order functions
// Applies an action to all elements of the list
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Usage:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Function-parameter
See? A function can be "kept" in a variable or passed like a normal value — just like handing over an apple in the kitchen!
Example 2: Pure function
A function that doesn't change program state and depends only on input:
int MultiplyByTwo(int x)
{
return x * 2;
}
- Doesn't depend on anything external.
- Doesn't change anything outside.
- For x = 5 it will always return 10.
Compare with a function that uses and changes a global variable:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
This is not a pure function — the result depends on external state and it mutates it.
Example 3: Data immutability
Instead of mutating input data we create new ones:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
The input list doesn't change at all. In multithreaded programs this is especially convenient: fewer locks and data races.
Example 4: Function composition
Get the sum of squares of all even numbers:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // Keep only even
.Select(n => n * n) // Square them
.Sum(); // Sum
}
Readable and declarative: each operation is a separate function.
3. Useful nuances
FP, LINQ and C#
LINQ is pretty much "FP in practice" for collections: you use higher-order functions (Where, Select, etc.), get new sequences without mutating the originals, and each transformation is a separate expression. The result is IEnumerable<T>, which describes what to get, not how to iterate.
Analogy table
| Imperative (procedural/OOP) | Functional (LINQ/FP-style) |
|---|---|
|
|
| "Mutate" a collection | Get a new collection |
| State (total += x) | Pure functions (xs.Sum()) |
| Describe as: "do this" | Describe: "what we want to get" |
FP vs OOP: two worlds — one C#
These aren't competing camps. In real C# projects you combine them: the domain model is convenient to build with classes (OOP), while collection processing, data aggregation and transformations are done in a functional style via LINQ, lambdas and extension methods.
Your knowledge of delegates is directly useful: Func<T, TResult>, Predicate<T>, Action<T> are typical building blocks of the FP style.
Generic filter function:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
Calls:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
Instead of many similar methods with different conditions — one generic function.
Why employers and interviews like FP developers?
- FP helps test small code units without starting the whole system.
- Easier to maintain logic: fewer states — fewer bug sources.
- Easier to write parallel and async code — no global state, fewer data races.
How not to get fanatical?
Yes, FP is powerful. But C# is not a purely functional language, and not every task requires perfect purity. Don't be afraid of local variables and reasonable mutation where appropriate. The main things are readability, predictability and testability. FP elements are a tool, not a religion.
4. Common beginner mistakes
It's very easy to end up with code that looks functional but actually isn't.
For example, a function returns a new collection but mutates the original list along the way — that breaks the immutability principle and surprises callers.
Another example: a lambda closes over an external variable and changes it. In the functional paradigm this is considered a side effect and makes code behavior less predictable.
The C# compiler won't stop you: the language allows both. That's why FP practices emphasize keeping a function "self-contained", not changing or reading anything outside except its arguments.
GO TO FULL VERSION