5 min read
A Practical Guide to Higher Order Functions in C#
If you have been programming for any length of time, you may well have come across higher order functions, but (like me) may not have fully appreciated just how powerful they can be. If you've not heard of them before, never fear, I have an explanation below.
What are higher order functions?
Simply put, a higher order function is a function whose input or output is also a function.
In the following example, the Map function takes a list of a particular data type and a function, and returns a new list with the function applied to each of the elements in the list (basically equivalent to LINQs Select method).
// definition
List<TOut> Map<TIn, TOut>(List<TIn> list, Func<TIn, TOut> mapper)
{
var newList = new List<TOut>();
foreach (var item in list)
{
var newItem = mapper(item);
newList.Add(newItem);
}
return newList;
}
// usage
var myList = new List<int> { 1, 2, 3, 4, 5 };
int multiplyBy2 (int num) => num * 2;
var multipliedList = Map(myList, multiplyBy2);
// output { 2, 4, 6, 8, 10 };
bool isEven (int num) => num % 2 == 0;
var isEvenList = Map(myList, isEven);
// output { false, true, false, true, false };
Here is an example of a higher order function that returns a function. First you call the Add function with a given integer, which returns a function that adds that first integer to any given other integer.
Func<int, int> Add(int a) => (int b) => a + b;
var add9 = Add(9);
var sum1 = add9(1);
// output 10
var sum2 = add9(2);
// output 11
Hopefully by now you can see why higher order functions are so useful; they allow complex code to be reused in a highly flexible way by letting the client define their own input/output functions. For example, without the use of higher order functions, one would have to define a new Map function for every single type of mapping they needed.
Practical Example - Repository Pattern
The repository pattern is a very common design pattern used as a data access abstraction. It allows you to perform your typical CRUD operations without the client having to interact directly with the data provider. A typical repository interface may look something like this:
public interface IProductRepository
{
int Create(Product product);
bool Update(Product product);
bool Delete(int id);
Product GetById(int id);
IEnumerable<Product> GetAll();
IEnumerable<Product> GetByCategoryId(int categoryId);
IEnumerable<Product> GetActive();
// etc...
}
As you can see in this typical example, there are numerous definitions that all return IEnumerable<Product>
, and every time you need another specific type of product filter, that requires adding to the interface and writing an entirely new implementation. That's not great for maintainability...
Instead, using higher order functions, we can define a single method that gets IEnumerable<Product>
that takes a filter function as an input. That way the client is free to define their own filters and the ProductRepository doesn't need to keep being updated with new implementations.
interface IProductRepository
{
// create, update, delete omitted
IEnumerable<Product> Get(Func<Product, bool> filter = null);
}
public class ProductRepository : IProductRepository
{
private readonly List<Product> _products = new List<Product>(); // data source
public IEnumerable<Product> Get(Func<Product, bool> filter = null)
{
// typically you might use the LINQ Where method here
// but using the foreach to be clear what is happening
if (filter is null) return _products;
var filteredList = new List<Product>();
foreach(var product in _products)
{
if(filter(product))
{
filteredList.Add(product);
}
}
return filteredList;
}
}
// client code
var allProducts = _productRepository.Get();
var productsByCategoryId = _productRepository.Get(p => p.CategoryId == 1);
var activeProducts = _productRepository.Get(p => p.Active);
Conclusion
In this article I have introduced the concepts of higher order functions and demonstrated how they are extremely useful in creating reusable and flexible code that is easy to maintain and understand. I have also given a practical use case that you may find in a .NET enterprise application - making the repository pattern more reusable.