So, here's another revelation: I've been using higher-order functions without realizing it.
What is a Higher Order Function?
The concept of a higher-order function is quite simple, but can also be a bit confusing:
A higher-order function is a function that takes a function as a parameter.We're used to passing objects as parameters. Sometimes they are simple objects like an integer or a string. Sometimes they are complex objects like collections or custom classes.
But we can also pass functions as parameters. This is done using delegates or lambda expressions. Let's take a look at some examples.
Injecting Behavior with Delegates
Delegates are an important part of the C# language. And they are important to understand when looking at LINQ and lambda expressions (two of my favorite things in C#). In my presentation, "Get Func<>-y: Delegates in .NET", I show how to use delegates to inject functionality into a class.
Here's the class:
Let's take a closer look at the ToString method:
This is a function that takes another function as a parameter (Func<Person, string>). That makes this a higher-order function.
With this method in place, we can choose exactly what we want the ToString method to do when we execute it. One of the easiest things to do is use a lambda expression to represent the parameter, so that's what I normally do. Here's the code that uses this method:
The first method call is to the ToString function that takes no parameters. We just have this here for reference. But all of the other calls use the ToString method that takes a function as a parameter.
We won't go into the details of how delegates and Func<T> work; that's covered in the presentation materials. The short version is that our parameter type is Func<Person, string>. What this means is that our parameter needs to be a function that takes a "Person" as a parameter and returns a "string".
So, that's what each of these do. The "p" parameter of the lambda expression represents the current "Person" object. Then the body of the lambda expression performs the work to return a string. The first example simply calls the no-parameter ToString method. But from there, we take a little more control.
In one method, we output the LastName property in upper case. In another, we output the FirstName property as lower case. And in another, we use a "LastName comma FirstName" format.
And here's out output:
Why do we do this? Several reasons go hand-in-hand. The Single Responsibility Principle (the S in the SOLID principles) tells us that our class should only have one reason to change. With regard to our "Person" class, its primary responsibility should not be formatting output strings. So, we moved that responsibility out of the class so someone else can be responsible for it.
We also have the Open Close Principle (the O in the SOLID principles) that tells us that a class should be open to extension but closed to modification. We can add custom string outputs to the Person class (extending it) without making any changes to the class itself.
And bringing things back around, a higher-order function (ToString(Func<Person, string>)) makes this possible.
LINQ and Where
I love LINQ. It's one of my favorite tools. And I also love lambda expressions. In my presentation "Learn to Love Lambdas", I show plenty of both.
And I've recently put the pieces together that LINQ methods are good examples of higher-order functions. Let's specifically look at "Where". First, here's the declaration (from MSDN):
This shows us that "Where" takes a "Func<TSource, bool>" as a parameter. This is a delegate (which is a function), meaning that Where is a higher-order function. We won't go into all the other details of this declaration (like the extension method syntax or the generic types); these are covered in the presentation.
But let's look at the Func<TSource, bool> parameter. Like our other example, this needs to be a function that takes a TSource as a parameter (where TSource is simply the type of the objects in our collection that we're working with) and returns a true/false value.
We have a simple example of filtering with "Where" using a text box. Here's the UI:
And here's the code that uses this:
In this method, "data" is a collection of Person objects (similar to the Person class above). This code checks to see if the Name Filter checkbox is checked. If so, then it puts a Where condition on our data. And just like above, "p" represents our Person object. And we can see the comparison of the FirstName property of our Person to whatever is in the text box. This gives us a true/false value.
Here's the resulting output:
This really shows us the usefulness of a higher-order function. The "Where" function lets us pass in whatever functionality we want to determine which items we want to include in the output and which items we want to exclude from the output.
Now, "Where" is much more complicated than the "ToString" method that we showed above. "ToString" simply ran whatever function was passed in. But "Where" does quite a bit of other things. It's involved in iterating through a sequence of some sort (IEnumerable). And it does this in a lazy way -- it only returns items as we ask for them. So, there's a lot more going on behind the scenes than simply running the function that we pass as a parameter. But that function plays an important role.
As a side note, this application has a lot of code-behind (since I want to concentrate on LINQ and lambdas during the presentation and not get mired into the application too deeply). But I've also created a version that uses a view model to separate the UI from the presentation logic (also included in the "Learn to Love Lambdas" materials).
Here's what the code from the view model looks like for the name filter:
We can see that this is very similar to the previous code. The primary difference is that it does not interact directly with UI elements. It uses properties instead. So, instead of looking at the state of the "NameFilterCheckBox", it looks at the "NameFilterChecked" property on the view model (which is databound to the checkbox). And instead of looking at the "NameTextBox" UI element, it looks at the "nameFilterValue" field on the view model (which again is set through databinding to the text box in the UI).
Why Do I Care?
So, I haven't shown any new code here. This is all code that I've been presenting for several years and based on real applications that I've built.
Is knowing that these are higher-order functions really important?The answer is YES. Now that I recognize these as higher-order functions, I can start to analyze them in that context. Higher-order functions have certain advantages, such as making the Strategy pattern easier to implement. And there are certain situations where we want to use higher-order functions, such as where we have similar functionality but we want to abstract out some of the details.
Now I can start to use these constructs more intentionally in my code. When I think about needing the Strategy pattern or I find similar functionality that can be combined, I can remember how higher-order functions can help with these.
So, even though I've been a fan of delegates, lambda expressions, and LINQ for a long time. I've been able to add another label to them: higher-order functions. It's now a new tool in my toolbox. I can learn the advantages and disadvantages of them. And I can better recognize where I should be using them.
And this is why we want to keep learning.
Post a Comment