The functional programming is a paradigm that builds programs through the composition of functions and limits state changes and side effects. For years it was presented as something academic, obscure, and reserved for languages like Haskell or Lisp.
But something changed. Processors stopped becoming faster and started having more cores. Concurrency became critical. And suddenly, the mutable state of OOP became a nightmare to manage.
Today, languages like C#, Java, JavaScript, and C++ have embraced functional features. Not to replace OOP, but to complement it.
In this article, we won’t talk about monads or functors. We’ll focus on predictable and easy-to-test code.
Pure Functions
The core concept of FP is the Pure Function. For a function to be considered pure, it must strictly follow two rules:
- Determinism: Given the same input arguments, it always returns the same result.
- No Side Effects: It does not modify anything outside its own scope (no changing global variables, no writing to disk, no printing to console).
Example in C#
// ❌ IMPURE FUNCTION
// Depends on an external variable 'taxRate'.
// If someone changes 'taxRate', the result changes even if the input is the same.
public decimal CalculateTotalImpure(decimal amount)
{
return amount * GlobalConfig.taxRate;
}
// ❌ IMPURE FUNCTION (Side Effect)
// Modifies system state (Console) and depends on time (DateTime.Now)
public void LogMessage(string message)
{
Console.WriteLine($"{DateTime.Now}: {message}");
}
// ✅ PURE FUNCTION
// Everything it needs comes through parameters.
// It doesn't touch anything outside. Always returns the same for 100 and 0.21.
public decimal CalculateTotalPure(decimal amount, decimal rate)
{
return amount * rate;
}Why does this matter?
Pure functions are easy to test. You don’t need mocks or to set up databases: you call CalculateTotalPure(100, 0.21) and verify it returns 21. You can also evaluate them concurrently without coordinating shared state, as long as the values they work with are truly immutable.
Immutability
In traditional OOP, we create objects and constantly modify them (setters). In FP, data does not change. If you want to change something, you create a new copy with the change applied.
This seems inefficient at first glance (“copy the whole object?”), but in modern architectures it helps enormously to avoid Race Conditions. If an object cannot change, two threads can read it simultaneously without fear of one corrupting it.
Immutability in C# with records
C# 9 introduced record. Positional records offer init properties and a convenient syntax for creating copies, although a record does not guarantee deep immutability: it can contain mutable members.
// Defining an immutable type
public record Person(string Name, int Age);
// Usage
var p1 = new Person("Luis", 30);
// ❌ ERROR: Cannot change the property (it's init-only)
// p1.Age = 31;
// ✅ CORRECT: "Non-destructive mutation" using 'with'
// Creates a NEW instance copying p1 but changing the age
var p2 = p1 with { Age = 31 };
Console.WriteLine(p1.Age); // 30 (p1 remains intact)
Console.WriteLine(p2.Age); // 31Higher-Order Functions
In FP, functions are first-class citizens. This means a function can:
- Be assigned to a variable.
- Be passed as an argument to another function.
- Be returned as the result of another function.
This allows us to abstract not only data but behavior.
Func<T, R> and Action<T>
In C#, we use generic delegates for this. Let’s see how to create a method that can filter anything, delegating the decision logic to the caller.
public class Filter
{
// This function receives a list AND ANOTHER FUNCTION (predicate)
public List<int> Filter(List<int> numbers, Func<int, bool> criterion)
{
var result = new List<int>();
foreach (var n in numbers)
{
// Execute the function that was passed
if (criterion(n))
{
result.Add(n);
}
}
return result;
}
}
// Usage:
var list = new List<int> { 1, 2, 3, 4, 5, 6 };
var filter = new Filter();
// Pass the "is even" behavior as an argument (Lambda)
var evens = filter.Filter(list, n => n % 2 == 0);
// Pass the "is greater than 3" behavior
var greaterThan = filter.Filter(list, n => n > 3);This is exactly what LINQ (.Where(), .Select()) does. LINQ is a functional library embedded in an object-oriented language.
Function Composition
If our functions are small Lego pieces (Pure), we can chain them to create complex processes.
In mathematics, if you have f(x) and g(x), you can do h(x) = g(f(x)).
In functional programming, this is called a Pipeline.
// Imperative (Nested and hard to read)
var result = SaveToDB(ConvertToJson(ValidateUser(user)));
// Functional (Extension methods in C# allow simulating pipelines)
// Data flows from left to right (or top to bottom)
var result = user
.Validate()
.ConvertToJson()
.SaveToDB();OOP or FP? We can combine both
They are often presented as enemies, but in modern development (especially in C#, TypeScript, Swift, or Kotlin), the ideal architecture is hybrid:
- Use OOP to model objects with identity and responsibilities: organize code into cohesive modules and define clear boundaries.
- Use FP for transformations: leverage immutability, LINQ, and pure functions, and separate side effects.
Functional Programming forces you to be disciplined. By restricting what you can do (no mutating variables, no hidden side effects), paradoxically it gives you more freedom: the freedom to refactor without fear and to parallelize your code without headaches.
You don’t need to rewrite all your code tomorrow. You can start by isolating deterministic calculations and passing their dependencies as parameters. A static method is not pure just because it’s static; it can still read the time, write to disk, or modify global state.
Make the core of your domain Functional (Pure and Immutable) and push side effects (DB, UI, Network) to the edges of the application. This is known as Functional Core, Imperative Shell.