Higher-Order Functions
Vaibhav • September 11, 2025
In the previous article, we introduced the foundational concepts of functional programming in C#, including pure functions, immutability, and closures. One of the most powerful ideas in functional programming - and one that C# supports elegantly - is the concept of higher-order functions. These are functions that either take other functions as parameters, return functions as results, or both.
Higher-order functions allow you to abstract behavior, build reusable logic, and compose operations in a clean, declarative way. In this article, we’ll explore how to write and use higher-order functions in C#, how they interact with delegates and lambdas, and how they can simplify common programming tasks like filtering, mapping, and chaining.
What Makes a Function “Higher-Order”?
A function is considered higher-order if it does at least one of the following:
- Accepts another function as a parameter
- Returns a function as its result
This allows you to treat behavior as data - passing logic around just like you would pass numbers or strings. In C#, this is made possible by delegates and lambda expressions, which allow functions to be stored in variables and passed as arguments.
Passing Functions as Parameters
Let’s start with a simple example of a higher-order function that accepts another function as a parameter. Suppose we want to apply a transformation to a number - but we want the transformation to be customizable.
int Apply(Func<int, int> operation, int value)
{
return operation(value);
}
The Apply
method takes a Func<int, int>
delegate and an integer. It applies the function to the value and returns the result. You can use it like this:
Console.WriteLine(Apply(x => x + 10, 5)); // Output: 15
Console.WriteLine(Apply(x => x * x, 4)); // Output: 16
This pattern is useful for abstracting behavior - you define the logic once, and customize it by passing different functions.
Returning Functions from Functions
Higher-order functions can also return functions. This is useful when you want to generate behavior dynamically - for example, creating a multiplier based on a given factor.
Func<int, int> MakeMultiplier(int factor)
{
return x => x * factor;
}
The MakeMultiplier
method returns a lambda that multiplies its input by the
specified factor. You can use it like this:
var doubleIt = MakeMultiplier(2);
var tripleIt = MakeMultiplier(3);
Console.WriteLine(doubleIt(5)); // Output: 10
Console.WriteLine(tripleIt(5)); // Output: 15
Each call to MakeMultiplier
returns a new function with its own captured
variable. This is a classic example of closures in action - the returned function retains access to the factor
variable.
Filtering with Higher-Order Functions
One of the most common uses of higher-order functions is filtering collections. Instead of writing loops, you
can pass a predicate function to a method like Find
or Where
.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evens = numbers.Where(n => n % 2 == 0).ToList();
List<int> greaterThanThree = numbers.Where(n => n > 3).ToList();
The Where
method accepts a Func<int, bool>
- a predicate that returns true
for elements that should be included. You can pass different predicates to
customize the filtering logic.
Mapping with Higher-Order Functions
Mapping is another common pattern - transforming each element in a collection using a function. In C#, you use
Select
for this.
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> labels = numbers.Select(n => $"Number: {n}").ToList();
The Select
method accepts a Func<int, string>
- a transformation function. Each element is passed to
the function, and the result is added to the new collection.
Chaining Higher-Order Functions
You can chain higher-order functions to build complex logic from simple steps. This is especially powerful when working with LINQ or pipelines.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers
.Where(n => n % 2 == 1)
.Select(n => n * n)
.ToList();
This code filters out even numbers, squares the remaining ones, and collects the results. Each step is a higher-order function - accepting a lambda and returning a new collection.
Composing Functions
You can also compose functions manually - combining them into a single function that applies multiple transformations. This is useful when you want to reuse logic across different parts of your code.
Func<int, int> doubleIt = x => x * 2;
Func<int, int> addTen = x => x + 10;
Func<int, int> composed = x => addTen(doubleIt(x));
Console.WriteLine(composed(3)); // Output: 16
The composed
function applies doubleIt
first,
then addTen
. This pattern is useful for building reusable pipelines -
especially when combined with delegates or configuration.
Returning Predicates Dynamically
Sometimes you want to generate a predicate based on input - for example, filtering numbers greater than a threshold. You can return a function that captures the threshold.
Func<int, bool> GreaterThan(int threshold)
{
return x => x > threshold;
}
var isGreaterThanThree = GreaterThan(3);
Console.WriteLine(isGreaterThanThree(5)); // Output: True
Console.WriteLine(isGreaterThanThree(2)); // Output: False
The returned function retains access to the threshold
variable - even after
GreaterThan
has finished executing. This is a closure in action, and it’s a
powerful way to customize behavior.
Using Higher-Order Functions for Configuration
Higher-order functions are useful for configuration - allowing you to inject behavior into a system without hardcoding it. For example, you might pass a logging function to a service:
void ProcessData(List<int> data, Action<string> logger)
{
foreach (var item in data)
{
logger($"Processing item: {item}");
}
}
You can pass different loggers depending on context:
ProcessData(new List<int> { 1, 2, 3 }, Console.WriteLine);
ProcessData(new List<int> { 1, 2, 3 }, msg => File.AppendAllText("log.txt", msg + "\n"));
This makes your code more flexible and testable - you can swap out behavior without changing the core logic.
Summary
Higher-order functions are a cornerstone of functional programming - and C# supports them beautifully through delegates and lambdas. In this article, you learned how to write functions that accept or return other functions, how to use them for filtering, mapping, and composition, and how they enable clean, declarative logic.
You also explored closures, dynamic predicates, and how higher-order functions can be used for configuration and abstraction. These patterns help you write code that’s modular, reusable, and easy to reason about - especially in large or complex systems.
In the next article, we’ll explore Closures and Captured Variables in more depth - showing how lambdas retain access to variables from their outer scope, and how this behavior can be used to maintain state, customize behavior, and build powerful functional constructs.