Functional
C# admits substantial functional-style programming despite being primarily object-oriented. The principal mechanisms are first-class delegates and lambdas, LINQ for higher-order operations, pattern matching for value-level dispatch, records and immutability for data, and tuples for structural decomposition. Combined with the discipline of pure functions, the constructs cover most of the practical functional programming patterns. The language is not purely functional and does not enforce immutability or referential transparency, but the patterns it supports are widely used in idiomatic C# — particularly the LINQ-driven style of expressing transformations as pipelines of operations rather than as mutation-driven loops.
This page surveys the functional surface and the conventions for using each mechanism. The deeper LINQ treatment is in LINQ; records are covered in Classes, structs, and records; pattern matching is in Pattern matching.
First-class functions
C# does not have free functions, but lambdas, delegates, and method references are first-class values:
Func<int, int> square = x => x * x;
Func<int, int> double_ = x => x * 2;
Func<int, int> chain = x => double_(square(x)); // 2x²
int n = chain(3); // 18
Functions can be stored, passed, returned, and combined:
public static Func<T, V> Compose<T, U, V>(Func<U, V> f, Func<T, U> g)
=> x => f(g(x));
var doubled_then_squared = Compose<int, int, int>(square, double_);
int n = doubled_then_squared(3); // (3*2)² = 36
The composition is the conventional functional pattern; C#‘s lack of currying and partial application means it is more verbose than in languages with first-class support, but the construction is well-defined and idiomatic in functional-style code.
Lambdas and closures
Lambdas capture variables from the enclosing scope:
public Func<int, int> MakeAdder(int delta) {
return x => x + delta;
}
var add5 = MakeAdder(5);
int result = add5(10); // 15
Captures are by reference: the lambda holds a reference to the captured variable, not a copy. Modifying the variable in the enclosing scope is visible from the lambda.
The compiler implements the closure as a generated class containing the captured variables and the lambda’s body as a method. The implication: lambdas with captures allocate; lambdas without captures are cached and may be reused.
// Caches a single delegate (no capture):
var pred = (int n) => n > 0;
// Allocates a closure object per call (captures threshold):
public Func<int, bool> AtLeast(int threshold) => n => n >= threshold;
C# 9 admitted static lambdas, which forbid capture and so do not allocate:
Func<int, int> sq = static x => x * x; // no capture; cached
The conventional discipline:
- Use ordinary lambdas freely; the allocation cost is rarely meaningful.
- Use
staticlambdas in hot paths and to make the absence-of-capture explicit. - Be aware of capturing loop variables; the canonical “C# Gotcha” is closing over a
forloop’s index.
Higher-order operations through LINQ
LINQ is the principal higher-order interface. The standard query operators correspond to the conventional functional primitives:
| Functional name | LINQ |
|---|---|
| map | Select |
| filter | Where |
| flatMap / bind | SelectMany |
| fold / reduce | Aggregate |
| group by | GroupBy |
| zip | Zip |
| take, drop | Take, Skip |
| sort by | OrderBy |
var sum = numbers.Aggregate(0, (acc, n) => acc + n);
var doubled = numbers.Select(n => n * 2);
var pairs = numbers.SelectMany(n => Enumerable.Range(1, n).Select(i => (n, i)));
The composition through the chain is the conventional functional style; the explicit foreach with accumulators is the imperative alternative.
Pattern matching as functional discrimination
Pattern matching is the C# discrimination mechanism for sum types. Combined with records and switch expressions, the mechanism approximates the algebraic-data-type pattern matching of functional languages:
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Square(double Side) : Shape;
public record Triangle(double Base, double Height) : Shape;
double Area(Shape s) => s switch {
Circle { Radius: var r } => Math.PI * r * r,
Square { Side: var side } => side * side,
Triangle { Base: var b, Height: var h } => 0.5 * b * h,
};
The treatment is in Pattern matching. The principal point: the combination of records (for the value-bearing types) and pattern-matching switch (for the discrimination) covers the conventional uses of algebraic data types in functional languages.
Records and immutability
Records are reference or value types with structural equality and (by default) immutable members:
public record Person(string FirstName, string LastName, int Age);
var alice = new Person("Alice", "Smith", 30);
var older = alice with { Age = 31 };
bool eq = alice == new Person("Alice", "Smith", 30); // true
The with expression admits non-destructive mutation — producing a new instance with selected members modified, leaving the original unchanged. The mechanism is the conventional functional pattern for “update one field of an immutable value”.
For genuinely persistent data structures, the standard library provides System.Collections.Immutable:
using System.Collections.Immutable;
var list1 = ImmutableList<int>.Empty.Add(1).Add(2).Add(3);
var list2 = list1.Add(4); // produces a new list; list1 unchanged
The immutable collections share structure where possible; the cost of an Add is logarithmic rather than linear, and concurrent access is safe without synchronisation.
Tuples and structural decomposition
Tuples are anonymous compound types with structural equality:
(int X, int Y) point = (3, 4);
var (x, y) = point; // structural decomposition
(int Sum, int Product) Compute(int a, int b) => (a + b, a * b);
var (sum, prod) = Compute(3, 4);
The construction is the conventional way to return multiple values from a function in functional style. Tuples participate in pattern matching:
string Combine((int X, int Y) p) => p switch {
(0, 0) => "origin",
(var x, 0) when x > 0 => "positive X axis",
(0, var y) when y > 0 => "positive Y axis",
_ => $"({p.X}, {p.Y})",
};
Pure functions and the discipline
C# does not enforce purity; functions may freely modify global state, perform I/O, and read clocks. Nonetheless, the discipline of writing pure functions — functions whose output depends only on their arguments and that have no side effects — has substantial benefits:
- Pure functions are easier to test (no fixtures, no mocks).
- Pure functions are easier to reason about.
- Pure functions admit memoisation and parallelisation.
- Pure functions compose freely.
The conventional discipline:
- Take all inputs as parameters; do not read global state.
- Return all outputs; do not mutate parameters.
- Avoid I/O in computation functions; perform I/O at the boundaries.
- Mark methods that produce results that should not be discarded with
[NoDiscard]-style annotations or expression-bodied returns.
public static decimal CalculateTotal(IEnumerable<Order> orders, decimal taxRate) {
return orders.Sum(o => o.Amount) * (1 + taxRate);
}
The function is pure: same inputs yield same outputs; no side effects. Most LINQ-style code naturally fits this pattern.
The Func<> and Action<> types
The standard generic delegate types substitute for first-class function types in most cases:
| Need | Type |
|---|---|
T → R | Func<T, R> |
T1, T2 → R | Func<T1, T2, R> |
T → () | Action<T> |
T → bool | Predicate<T> (or Func<T, bool>) |
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = s => Console.WriteLine(s);
Predicate<int> isPositive = n => n > 0;
The types are interchangeable up to their argument and return types; conversions are admitted in many cases. Production code chooses the type whose name and arity most clearly express the role.
Optional and result types
C# admits optional and result-style types through:
Nullable<T>(orT?) for value types: anint?is “an int or nothing”.T?for nullable reference types: astring?is “a string or null”, under the nullability discipline.Result<T, E>andOption<T>types from third-party libraries (LanguageExt, OneOf), or hand-rolled.
The standard library does not include a Result<T, E> or Either<L, R>; functional-style C# code uses third-party libraries or rolls its own.
public record Ok<T>(T Value);
public record Err<E>(E Error);
public record Result<T, E>;
// usage:
public Result<int, string> Parse(string s) =>
int.TryParse(s, out int n) ? new Ok<int>(n) : new Err<string>("parse failed");
The conventional pattern for representing fallibility is exception-throwing in C#, not value-returning result types; functional-style code often replaces exceptions with explicit Result types for the boundaries where the discipline is desired.
Functional composition
C# does not provide function composition operators, but the construction is straightforward:
public static class Functions {
public static Func<A, C> Compose<A, B, C>(Func<B, C> f, Func<A, B> g)
=> x => f(g(x));
public static Func<A, C> Pipe<A, B, C>(Func<A, B> f, Func<B, C> g)
=> x => g(f(x));
}
var parse_then_double = Functions.Pipe<string, int, int>(int.Parse, n => n * 2);
int n = parse_then_double("21"); // 42
The conventional libraries (LanguageExt, MoreLINQ) provide more elaborate composition primitives. For ordinary application code, LINQ chaining covers most of the cases:
var transformed = source
.Where(IsValid)
.Select(Transform)
.Where(IsAcceptable);
The chain is composition expressed through the LINQ pipeline.
Currying and partial application
Currying — converting (a, b) → c into a → b → c — is not built-in but is straightforward:
public static Func<A, Func<B, C>> Curry<A, B, C>(Func<A, B, C> f)
=> a => b => f(a, b);
var addCurried = Curry<int, int, int>((a, b) => a + b);
var add5 = addCurried(5);
int n = add5(10); // 15
Partial application — supplying some arguments to produce a function of the rest — is similarly manual:
public static Func<B, C> ApplyFirst<A, B, C>(Func<A, B, C> f, A a)
=> b => f(a, b);
var divide = (int a, int b) => a / b;
var halve = ApplyFirst(divide, 2); // wait — that's "2 / b", not "b / 2"
The construction is rare in idiomatic C#; the conventional pattern is to define a closure that captures the bound argument:
var add5 = (int x) => 5 + x; // simpler than Curry/ApplyFirst
A note on what C# does not have
The features that purely functional languages rely on, and their status in C#:
| Feature | Available? |
|---|---|
| First-class functions | Yes (delegates, lambdas) |
| Closures | Yes |
| Higher-order functions | Yes (LINQ) |
| Currying / partial application | Manual |
| Immutable data | Limited (readonly, init, records, immutable collections); not enforced globally |
| Algebraic data types | Through records and pattern matching |
| Pattern matching | Yes |
| Tail-call optimisation | Implementation-defined; not guaranteed |
| Persistent data structures | Through System.Collections.Immutable |
| Lazy evaluation | Through IEnumerable<T> (deferred LINQ) and Lazy<T> |
| Type classes | No (the closest is C# 11’s static abstract members on interfaces) |
| Monads | Manual (LINQ admits monad-like usage through SelectMany) |
The combination — LINQ, records, pattern matching, immutable collections, the Func<>/Action<> types — covers most of the practical functional programming use cases in C#. Code that genuinely requires more (Haskell-style purity, dependent types, true persistence) is typically written in a functional language and called from C# at a boundary.