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.