C# Functional Programming with Delegates and Higher-order functions

C# is often known for its object-oriented roots, but it also has a very strong functional programming capability. At the heart of this functionality are delegates, which enable higher-order functions (HOFs). If you've ever used LINQ in C#, you've already used HOFs without realizing it. What Are Higher-Order Functions? In functional programming, a higher-order function is a function that takes one or more functions as arguments, returns a function, or both. This concept allows for more abstract, reusable, and declarative code. In C#, HOFs are implemented using delegates, lambda expressions, and expression trees. Delegates A delegate in C# is a type that represents a reference to a method with a particular parameter list and return a particular type. Delegates are used to pass methods as arguments to other methods, enabling the creation of flexible and reusable code. public delegate int MathOperation(int x, int y); public int Add(int a, int b) => a + b; public int Multiply(int a, int b) => a * b; public static int PerformOperation(int a, int b, MathOperation operation) { return operation(a, b); } public static void Main() { MathOperation addOperation = Add; MathOperation multiplyOperation = Multiply; int sum = PerformOperation(5, 3, addOperation); int product = PerformOperation(5, 3, multiplyOperation); Console.WriteLine($"Sum: {sum}, Product: {product}"); } In this example, PerformOperation is a HOF that takes MathOperation delegate as an argument, allowing different operations to be passed and executed. In C#, there are three primary ways to define and use delegates: Custom delegate types using the delegate keyword (like the example above) Built-in delegates: Func, Action, and Predicate Anonymous methods/Lambda expressions for inline definitions Using built-in delegates Func is used when the delegate returns a value Action is used when the delegate returns void Predicate is used when the delegate returns a boolean. We usually use it for semantic, when we have a set of criteria and want to determine whether the parameters meet those criteria. In practice, it is the same as Func. Func multiply = (a, b) => a * b; int product = multiply(4, 5); Action log = message => Console.WriteLine(message); log("Hello from Action!"); Lambda Expressions Lambda expressions provide a concise way to represent anonymous methods (functions without a name). They are particularly useful when working with LINQ queries and functional constructs. var square = (int x) => x * x; Console.WriteLine(square(5)); // using with LINQ var numbers = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; var evenNumbers = numbers.Where(x => x % 2 == 0); Under the Hood of LINQ: Higher-Order Functions in Action Language Integrated Query (LINQ) is a powerful feature in C# that allows us to query and manipulate data in a declarative manner. When you use LINQ methods like .Where(), .Select(), or .Any(), you are passing delegates into higher-order functions. In the previous example, we used .Where(), this is what its signature looks like under the hood: public static IEnumerable Where( this IEnumerable source, Func predicate) Func is the delegate type. The lambda x => x % 2 == 0 matches the signature and is passed as the predicate. Real-World example In our case, we are building a system based on a card game. We want to make it so when a user hovers a card in hand, it highlights the cards in the deck that have synergy with the hovered card. We added a delegate in our cardlist class, that receives a card, the decklist and returns the color that we will highlight the cards: public Func? ShouldHighlightCard; This gave us the flexibility to implement the function to decide if we will highlight a card in each card: public class Thunderbringer : ICardWithHighlight { public string GetCardId() => HearthDb.CardIds.Collectible.Neutral.Thunderbringer; public HighlightColor ShouldHighlight(Card card, IEnumerable deck) => HighlightColorHelper.GetHighlightColor(card.IsElemental() && card.IsBeast(), card.IsElemental(), card.IsBeast()); } public class Birdwatching : ICardWithHighlight { public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.Birdwatching; public HighlightColor ShouldHighlight(Card card, IEnumerable deck) => HighlightColorHelper.GetHighlightColor(card.Type == "Minion"); } In this example, the Thunderbringer card highlights cards that are both Elemental and Beast in one color, only Elemental in another, only Beast in a third color, and leaves the rest unhighlighted. The Birdwatching card highlights any card of type Minion. If you're curious, here's the implementation of the HighlightColorHelper class: public static class HighlightColorHelper { private static readonly Dictionary _colorMapping = new() { { 0, HighlightColor.Teal },

May 8, 2025 - 01:03
 0
C# Functional Programming with Delegates and Higher-order functions

C# is often known for its object-oriented roots, but it also has a very strong functional programming capability. At the heart of this functionality are delegates, which enable higher-order functions (HOFs). If you've ever used LINQ in C#, you've already used HOFs without realizing it.

What Are Higher-Order Functions?

In functional programming, a higher-order function is a function that takes one or more functions as arguments, returns a function, or both.

This concept allows for more abstract, reusable, and declarative code. In C#, HOFs are implemented using delegates, lambda expressions, and expression trees.

Delegates

A delegate in C# is a type that represents a reference to a method with a particular parameter list and return a particular type. Delegates are used to pass methods as arguments to other methods, enabling the creation of flexible and reusable code.

public delegate int MathOperation(int x, int y);

public int Add(int a, int b) => a + b;
public int Multiply(int a, int b) => a * b;

public static int PerformOperation(int a, int b, MathOperation operation)
{
    return operation(a, b);
}

public static void Main()
{
    MathOperation addOperation = Add;
    MathOperation multiplyOperation = Multiply;

    int sum = PerformOperation(5, 3, addOperation);
    int product = PerformOperation(5, 3, multiplyOperation);
    Console.WriteLine($"Sum: {sum}, Product: {product}");
}

In this example, PerformOperation is a HOF that takes MathOperation delegate as an argument, allowing different operations to be passed and executed.

In C#, there are three primary ways to define and use delegates:

  • Custom delegate types using the delegate keyword (like the example above)
  • Built-in delegates: Func<>, Action<>, and Predicate<>
  • Anonymous methods/Lambda expressions for inline definitions

Using built-in delegates

  • Func is used when the delegate returns a value
  • Action is used when the delegate returns void
  • Predicate is used when the delegate returns a boolean. We usually use it for semantic, when we have a set of criteria and want to determine whether the parameters meet those criteria. In practice, it is the same as Func.
Func<int, int, int> multiply = (a, b) => a * b;
int product = multiply(4, 5);

Action<string> log = message => Console.WriteLine(message);
log("Hello from Action!");

Lambda Expressions

Lambda expressions provide a concise way to represent anonymous methods (functions without a name). They are particularly useful when working with LINQ queries and functional constructs.

var square = (int x) => x * x;
Console.WriteLine(square(5));

// using with LINQ
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var evenNumbers = numbers.Where(x => x % 2 == 0);

Under the Hood of LINQ: Higher-Order Functions in Action

Language Integrated Query (LINQ) is a powerful feature in C# that allows us to query and manipulate data in a declarative manner. When you use LINQ methods like .Where(), .Select(), or .Any(), you are passing delegates into higher-order functions.

In the previous example, we used .Where(), this is what its signature looks like under the hood:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
  • Func is the delegate type.
  • The lambda x => x % 2 == 0 matches the signature and is passed as the predicate.

Real-World example

In our case, we are building a system based on a card game. We want to make it so when a user hovers a card in hand, it highlights the cards in the deck that have synergy with the hovered card.

We added a delegate in our cardlist class, that receives a card, the decklist and returns the color that we will highlight the cards:

public Func<Hearthstone.Card, IEnumerable<Hearthstone.Card>, HighlightColor>? ShouldHighlightCard;

This gave us the flexibility to implement the function to decide if we will highlight a card in each card:

public class Thunderbringer : ICardWithHighlight
{
    public string GetCardId() => HearthDb.CardIds.Collectible.Neutral.Thunderbringer;

    public HighlightColor ShouldHighlight(Card card, IEnumerable<Card> deck) =>
        HighlightColorHelper.GetHighlightColor(card.IsElemental() && card.IsBeast(), card.IsElemental(), card.IsBeast());
}

public class Birdwatching : ICardWithHighlight
{
    public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.Birdwatching;

    public HighlightColor ShouldHighlight(Card card, IEnumerable<Card> deck) =>
        HighlightColorHelper.GetHighlightColor(card.Type == "Minion");
}

In this example, the Thunderbringer card highlights cards that are both Elemental and Beast in one color, only Elemental in another, only Beast in a third color, and leaves the rest unhighlighted. The Birdwatching card highlights any card of type Minion. If you're curious, here's the implementation of the HighlightColorHelper class:

public static class HighlightColorHelper
{
    private static readonly Dictionary<int, HighlightColor> _colorMapping = new()
    {
        { 0, HighlightColor.Teal },
        { 1, HighlightColor.Orange },
        { 2, HighlightColor.Green },
    };

    public static HighlightColor GetHighlightColor(params bool[] conditions)
    {
        if (conditions.Length == 0)
            return HighlightColor.None;

        for (var i = 0; i < conditions.Length; i++)
        {
            if(!conditions[i]) continue;
            if (_colorMapping.TryGetValue(i, out var color))
            {
                return color;
            }
        }

        return HighlightColor.None;
    }
}

Conclusion

C#'s support for delegates and higher-order functions opens up a world of possibilities for developers looking to write more functional and expressive code. By embracing these concepts, you can write code that is not only more powerful but also more elegant and maintainable. Understanding these delegate forms helps you:

  • Read and write more expressive LINQ queries
  • Pass behavior around in your applications like data
  • Create APIs that are extensible and composable

So next time you use .Where() or .Select(), you'll know that you're already writing functional code in C#.