From the course: Advanced C#: Functional Programming Patterns

Functional function concepts

In this chapter, we'll look at how to write C# methods in a functional way. In other words, how to create functional functions. Let's review the concept of pure functions. We strive for pure functions in programming, particularly in functional programming, because they offer key benefits that lead to more reliable, maintainable, and understandable code. Pure functions don't modify any state or interact with external systems like databases or file systems. This lack of side effects reduces the chances of unexpected behavior, making the function easier to test and debug. Pure functions always produce the same output given the same input, making them highly predictable. This consistency helps in reasoning about the code, as you don't need to consider external factors. Since pure functions don't rely on or modify external state, you can easily test them by simply passing inputs and verifying the outputs. There is no need to set up complex environments or mock external dependencies. Pure functions can be tested in isolation, ensuring that tests focus on the function's behavior without interference from other parts of the system. Because pure functions don't modify shared state, they can be safely executed in parallel or concurrently. This reduces the complexity of building multithreaded or distributed systems. Pure functions eliminate the risks of race conditions since they don't alter global state or rely on mutable data. We will always strive to build pure functions whenever possible, and when it's not feasible, will isolate functions with side effects into specific areas. Beyond pure functions, there are other important concepts to consider. When someone says first-class functions in functional programming, that means that functions are treated as fundamental building blocks in the language. This concept allows functions to be used just like any other data types such as integers, strings, or objects. Here's what that means in practice. You can assign a function to a variable, store it in a data structure like an array or an object. Once it's in the variable, it can be called later. Additionally, there is the concept of higher-order functions. These are functions that can either take other functions as arguments or return them as results. This is a key feature of functional programming, allowing for more abstract and flexible code. You can create a higher-order function that accepts other functions as input parameters. Then we can define another function and pass it in as an argument to the first function, just like you would with any other data type. For example, you might pass a function that defines a specific operation, like adding or multiplying to a higher-order function. Additionally, a function can return another function as its output. This enables more dynamic and customizable code, as the returned function's behavior can be tailored based on the input it receives. So how do we implement these principles in C#? First, let's explore the tools available for defining functions. We'll start with the standard method, the most common way to define functions in C#. It remains a solid choice and it is useful in functional C#. Extension methods in C# are useful for function chaining, allow you to link multiple method calls together in a single fluid expression. This is aligned with the principles of expression-based code in functional programming, where the focus is on creating clear and concise code. By using extension methods, we can craft APIs that are not only fluent and readable, but also maintainable and expressive. LINQ makes extensive use of extension methods for this reason. A delegate is a type that safely encapsulates a method. This makes it perfect for storing functions in variables and defining as function parameters and return types. A widely-used generic delegate in functional C# is the func delegate. It's versatile and handles most scenarios effectively. In this example, func represents a function with three input parameters and one return value. The action delegate is less useful as it doesn't return a result from the function call. It's analogous to a method that returns void. In functional patterns, these are typically used for isolating code that performs an action or side effect. Predicate<T> delegate represents a method that takes one input parameter of type T and returns a Boolean value. It's commonly used for filtering or condition checking. This makes it ideal for writing custom methods that filter data. While LINQ is commonly used for filtering, using a predicate as a parameter is a strong alternative when you need to build your own filtering functions. C# lambda expressions are a concise way to define anonymous functions, functions without a name that can be used wherever a delegate or expression tree is expected. They are especially useful for writing short functions inline without the need to define a separate method. This makes them ideal for defining functions that can be passed as arguments to other functions. Before we look at code, let's recap. Our goal is to build pure functions that produce consistent results and avoid side effects. We can leverage various .NET and C# tools to compose our system effectively. For some parts of our code, we'll use standard methods, and for others, we'll create extension methods to support function chaining. We'll build higher-order functions utilizing .NET delegates like Func<T> and Predicate<T> for function parameters and return types. To make our higher order functions flexible, we'll use lambda expressions to define the functions passed into it. Now let's look at some examples.

Contents