1 Introducing functional programming

This chapter covers

  • Benefits and tenets of functional programming
  • Functional features of the C# language
  • Using records and pattern matching for type-driven programs

Functional programming (FP) is a programming paradigm: a different way of thinking about programs than the mainstream imperative paradigm you’re probably used to. For this reason, learning to think functionally is challenging but also very enriching. My ambition is that, after reading this book, you’ll never look at code with the same eyes as before!

The learning process can be a bumpy ride. You’re likely to go from frustration at concepts that seem obscure or useless to exhilaration when something clicks in your mind, and you’re able to replace a mess of imperative code with just a couple of lines of elegant, functional code. This chapter will address some questions you may have as you start on this journey. What exactly is functional programming? Why should I care? Can I code functionally in C#? Is it worth the effort?

1.1 What is this thing called functional programming?

What exactly is functional programming (FP)? At a high level, it’s a programming style that emphasizes functions while avoiding state mutation. This definition is already twofold as it includes two fundamental concepts:

  • Functions as first-class values

  • Avoiding state mutation

Let’s see what these mean.

Running snippets in the REPL

As you go through the snippets in this chapter and in the book, I encourage you to type them in a REPL. A REPL (Read-Eval-Print-Loop) is a command-line interface that lets you experiment with the language by typing in statements and getting immediate feedback. You may want to try out a few variations on the examples I show you; getting your hands dirty by messing with real code will get you learning fastest.

If you use Visual Studio, you can start the REPL by going to View > Other Windows > C# Interactive. Alternatively, you can use LINQPad. Unfortunately, at the time of writing, these options are only available on Windows. On other OSs, you can use the csi command, even though it’s not as feature-rich.

1.1.1 Functions as first-class values

In a language where functions are first-class values, you can use them as inputs or outputs of other functions, you can assign them to variables, and you can store them in collections. In other words, you can do with functions all the operations that you can do with values of any other type.

For example, type the contents of the following listing into the REPL.

Listing 1.1 A simple example of using a function as a first-class value

var triple = (int x) => x * 3;             
 
var range = Enumerable.Range(1, 3);        
 
var triples = range.Select(triple);        
 
triples // => [3, 6, 9]

Defines a function that returns the triple of a given integer

Creates a list with the values [1, 2, 3]

Applies triple to all the values in range

In this example, you invoke Select (an extension method on IEnumerable), giving it the range and the triple function as arguments. This creates a new IEnumerable containing the elements obtained by applying the triple function to each element in the input range.

Notice that prior to C# 10 you needed to explicitly declare the delegate type for triple like so:

[source,csharp]
 
Func<int, int> triple = x => x * 3;

The code in listing 1.1 demonstrates that functions are indeed first-class values in C# because you can assign the multiply-by-3 function to the variable triple and give it as an argument to Select. Throughout the book, you’ll see that treating functions as values allows you to write powerful and concise code.

1.1.2 Avoiding state mutation

If we follow the functional paradigm, we should refrain from state mutation altogether: once created, an object never changes, and variables should never be reassigned (so that, in fact, they’re variable in name only). The term mutation indicates that a value is changed in place, updating a value stored somewhere in memory. For example, the following code creates and populates an array, and then it updates one of the array’s values in place:

int[] nums = { 1, 2, 3 };
nums[0] = 7;
 
nums // => [7, 2, 3]

Such updates are also called destructive updates because the value stored prior to the update is destroyed. These should always be avoided when coding functionally. (Purely functional languages don’t allow in-place updates at all.)

Following this principle, sorting or filtering a list should not modify the list in place but should create a new, suitably filtered or sorted list without affecting the original. Type the code in the following listing into the REPL to see what happens when sorting or filtering a list using LINQ’s Where and OrderBy functions.

Listing 1.2 Functional approach: Where and OrderBy create new lists

var isOdd = (int x) => x % 2 == 1;
int[] original = { 7, 6, 1 };
 
var sorted = original.OrderBy(x => x);
var filtered = original.Where(isOdd);
 
original // => [7, 6, 1]      
sorted   // => [1, 6, 7]      
filtered // => [7, 1]         

The original list isn’t affected.

Sorting and filtering yielded new lists.

As you can see, the original list is unaffected by the sorting or filtering operations, which yield new IEnumerables. Let’s look at a counterexample in the following listing. If you have an array, you can sort it in place by calling its Sort method.

Listing 1.3 Nonfunctional approach: List<T>.Sort sorts the list in place

int[] original = { 5, 7, 1 };
Array.Sort(original);
 
original // => [1, 5, 7]

In this case, after sorting, the original ordering is destroyed. We’ll see why this is problematic next.

NOTE The reason you see both the functional and nonfunctional approaches in .NET libraries is historical: Array.Sort predates LINQ, which marked a decisive turn in a functional direction.

1.1.3 Writing programs with strong guarantees

Of the two concepts we just discussed, functions as first-class values initially seems more exciting, and we’ll concentrate on it in chapter 2. But before we move on, I’d like to briefly demonstrate why avoiding state mutation is also hugely beneficial—it eliminates many complexities caused by mutable state.

Let’s look at an example. (We’ll revisit these topics in more detail, so don’t worry if not everything is clear at this point.) Type the code in the following listing into the REPL.

Listing 1.4 Mutating state from concurrent processes

using static System.Linq.Enumerable;                 
using static System.Console;                         
 
var nums = Range(-10000, 20001).Reverse().ToArray();
// => [10000, 9999, ... , -9999, -10000]
 
var task1 = () => WriteLine(nums.Sum());
var task2 = () => { Array.Sort(nums); WriteLine(nums.Sum()); };
 
Parallel.Invoke(task1, task2);                       
// prints: 5004 (or another unpredictable value)
//         0

Lets you call Range and WriteLine without full qualification

Executes both tasks in parallel

Here you define nums to be an array of all integers between 10,000 and -10,000; their sum should obviously be 0. You then create two tasks:

  • task1 computes and prints the sum.

  • task2 first sorts the array and then computes and prints the sum.

Each of these tasks correctly computes the sum if run independently. When you run both tasks in parallel, however, task1 comes up with an incorrect and unpredictable result. It’s easy to see why. As task1 reads the numbers in the array to compute the sum, task2 is reordering the elements in the array. That’s somewhat like trying to read a book while somebody else flips the pages: you’d be reading some well-mangled sentences! This is shown graphically in figure 1.1.

Figure 1.1 Modifying data in place can give concurrent threads an incorrect view of the data.

What if we use LINQ’s OrderBy method, instead of sorting the list in place? Let’s look at an example.

var task3 = () => WriteLine(nums.OrderBy(x => x).Sum());
Parallel.Invoke(task1, task3);
 
// prints: 0
//         0

As you can see, using LINQ’s functional implementation gives you a predictable result, even when you execute the tasks in parallel. This is because task3 isn’t modifying the original array but rather creating a completely new view of the data, which is sorted: task1 and task3 read from the original array concurrently, but concurrent reads don’t cause any inconsistencies, as figure 1.2 shows.

Figure 1.2 The functional approach: creating a new, modified version of the original structure

This simple example illustrates a wider truth: when developers write an application in the imperative style (explicitly mutating the program state) and later introduce concurrency (due to new requirements or a need to improve performance), they inevitably face a lot of work and potentially some difficult bugs. When a program is written in a functional style from the outset, concurrency can often be added for free or with substantially less effort. We’ll discuss state mutation and concurrency more in chapters 3 and 11. For now, let’s go back to our overview of FP.

Although most people will agree that treating functions as first-class values and avoiding state mutation are fundamental tenets of FP, their application gives rise to a series of practices and techniques, so it’s debatable which techniques should be considered essential and included in a book like this. I encourage you to take a pragmatic approach to the subject and try to understand FP as a set of tools that you can use to address your programming tasks. As you learn these techniques, you’ll start to look at problems from a different perspective: you’ll start to think functionally.

Now that we have a working definition of FP, let’s look at the C# language itself and at its support for FP techniques.

Functional vs. object-oriented?

I’m often asked to compare and contrast FP with object-oriented programming (OOP). This isn’t simple, mainly because there are conflicting assumptions about what OOP should look like.

In theory, the fundamental principles of OOP (encapsulation, data abstraction, and so on) are orthogonal to the principles of FP, so there’s no reason why the two paradigms can’t be combined.

In practice, however, most object-oriented (OO) developers heavily rely on the imperative style in their method implementations, mutating state in place and using explicit control flow; they use OO design in the large and imperative programming in the small. The real question is that of imperative versus functional programming.

Another interesting question is how FP differs from OOP in terms of structuring a large, complex application. The difficult art of structuring a complex application relies on the following principles. They’re generally valid, regardless of whether the component in question is a function, a class, or an application:

  • Modularity—Software should be composed of discrete, reusable components.

  • Separation of concerns—Each component should only do one thing.

  • Layering—High-level components can depend on low-level components but not vice versa.

  • Loose coupling—A component shouldn’t know about the internal details of the components it depends on; therefore, changes to a component shouldn’t affect components that depend on it.

These principles are also in no way specific to OOP, so the same principles can be used to structure an application written in the functional style. The difference will be in what the components are and which APIs they expose. In practice, the functional emphasis on pure functions (which we’ll discuss in chapter 3) and composability (chapter 7) make it significantly easier to achieve some of these design goals.a

  

a For a more thorough discussion on why imperatively flavored OOP is a cause of (rather than a solution to) program complexity, see the article “Out of the Tar Pit,” by Ben Moseley and Peter Marks (November, 2006) at http://mng.bz/xXK7.

1.2 How functional a language is C#?

Functions are indeed first-class values in C#, as demonstrated in the previous listings. In fact, C# had support for functions as first-class values from the earliest version of the language through the Delegate type, and the subsequent introduction of lambda expressions made the syntactic support even better. We’ll review these language features in chapter 2.

There are some quirks and limitations when it comes to type inference, which we’ll discuss in chapter 10, but overall the support for functions as first-class values is pretty good.

As for supporting a programming model that avoids in-place updates, the fundamental requirement in this area is that a language have garbage collection. Because you create modified versions of existing data structures, rather than updating their values in place, you want old versions to be garbage-collected as needed. Again, C# satisfies this requirement.

Ideally, the language should also discourage in-place updates. For a long time, this was C#'s greatest shortcoming: having everything mutable by default and no easy way to define immutable types was a hurdle when programming in a functional style. This all changed with the introduction of records in C# 9. As you’ll see in section 1.2.3, records allow you to define custom immutable types without any boilerplate; in fact, it’s easier to define a record than a “normal” class.

As a result of the features added over time, C# 9 offers good language support for many functional techniques. In this book, you’ll learn to harness these features and to work around any shortcomings. Next, we’ll review some language features of C# that are particularly relevant to FP.

1.2.1 The functional nature of LINQ

When C# 3 was released, along with version 3.5 of the .NET Framework, it included a host of features inspired by functional languages, including the LINQ library (System.Linq) and some new language features enabling or enhancing what you could do with LINQ. These features included extension methods, lambda expression, and expression trees.

LINQ is indeed a functional library (as you probably noticed, I used LINQ earlier to illustrate both tenets of FP). The functional nature of LINQ will become even more apparent as you progress through this book.

LINQ offers implementations for many common operations on lists (or, more generally, on “sequences,” as instances of IEnumerable should technically be called), the most common of which are mapping, sorting, and filtering, see the “Common operations on sequences” sidebar. Here’s an example combining all three operations:

Enumerable.Range(1, 100).
   Where(i => i % 20 == 0).
   OrderBy(i => -i).
   Select(i => $"{i}%")
// => ["100%", "80%", "60%", "40%", "20%"]

Notice how Where, OrderBy, and Select all take functions as arguments and don’t mutate the given IEnumerable but return a new IEnumerable instead. This illustrates both tenets of FP you saw earlier.

LINQ facilitates querying not only objects in memory (LINQ to objects), but various other data sources as well, like SQL tables and XML data. C# programmers have embraced LINQ as the standard toolset for working with lists and relational data (accounting for a substantial amount of a typical codebase). On the upside, this means that you’ll already have some sense of what a functional library’s API feels like.

On the other hand, when working with other types, C# programmers generally stick to the imperative style of using flow-control statements to express the program’s intended behavior. As a result, most C# codebases I’ve seen are a patchwork of functional style (when working with IEnumerables and IQueryables) and imperative style (when working with everything else).

What this means is that, although C# programmers are aware of the benefits of using a functional library such as LINQ, they haven’t had enough exposure to the design principles behind LINQ to leverage those techniques in their own designs. That’s something this book aims to address.

Common operations on sequences

The LINQ library contains many methods for performing common operations on sequences such as the following:

  • Mapping—Given a sequence and a function, mapping yields a new sequence whose elements are obtained by applying the given function to each element in the original sequence (in LINQ, this is done with the Select method):

    Enumerable.Range(1, 3).Select(i => i * 3) // => [3, 6, 9]
  • Filtering—Given a sequence and a predicate, filtering yields a new sequence including all the elements from the original sequence that satisfy the predicate (in LINQ, this is done with Where):

    Enumerable.Range(1, 10).Where(i => i % 3 == 0) // => [3, 6, 9]
  • Sorting—Given a sequence and a key-selector function, sorting yields a sequence where the elements of the original sequence are ordered by the key (in LINQ, this is done with OrderBy and OrderByDescending):

    Enumerable.Range(1, 5).OrderBy(i => -i) // => [5, 4, 3, 2, 1]

1.2.2 Shorthand syntax for coding functionally

C# 6, C# 7, and C# 10 were not revolutionary releases, but they included many smaller language features that, taken together, provide more idiomatic syntax and hence a better experience for coding functionally. The following listing illustrates some of these features.

Listing 1.5 C# idioms relevant for FP

using static System.Math;                       
 
public record Circle(double Radius)
{
   public double Circumference                  
      => PI * 2 * Radius;                       
 
   public double Area
   {
      get
      {
         double Square(double d) => Pow(d, 2);  
         return PI * Square(Radius);
      }
   }
}

using static enables unqualified access to the static members of System.Math, like PI and Pow.

An expression-bodied property

A local function is a method declared within another method.

Importing static members with the using static directive

The using static directive introduced in C# 6 allows you to import the static members of a class (in listing 1.5, the System.Math class). As a result, in this example you can invoke the PI and Pow members of Math without further qualification:

using static System.Math;
 
public double Circumference
   => PI * 2 * Radius;

Why is this important? In FP, we prefer functions whose behavior relies only on their input arguments because we can reason about and test these functions in isolation (contrast this with instance methods, whose implementation typically interacts with instance variables). These functions are implemented as static methods in C#, so a functional library in C# consists mainly of static methods.

using static allows you to more easily consume such libraries. This is even truer in C# 10, where global using static allows you to make functions available throughout your project. Although overuse of these directives can lead to namespace pollution, reasonable use can make for clean, readable code.

More concise functions with expression-bodied members

We declare the Circumference property with an expression body, introduced with =>, rather than with the usual statement body enclosed by curly braces:

public double Circumference
   => PI * 2 * Radius;

Notice how much more concise this is compared to the Area property in listing 1.5!

In FP, we tend to write lots of simple functions, many of them one-liners, and then compose these into more complex workflows. Expression-bodied methods allow you to do this with minimal syntactic noise. This is particularly evident when you want to write a function that returns a function—something you’ll see a lot of in this book.

The expression-bodied syntax was introduced in C# 6 for methods and property getters. It was generalized in C# 7 to also apply to constructors, destructors, getters, and setters.

Declaring functions within functions

Writing lots of simple functions means that many functions are called from one location only. C# allows you to make this explicit by declaring a function within the scope of another function. There are actually two ways to do this; the one I favor uses a delegate:

[source,csharp]
 
get
{
   var square = (double d) => Pow(d, 2);
   return PI * square(Radius);
}

This code uses a lambda expression to represent the function and assigns it to the square variable. (In C# 10, the compiler infers the type of square to be Func<double, double> so that you can declare it with the var keyword.) We'll look at lambda expressions and delegates more in depth in chapter 2.

Another possibility is to use local functions, effectively methods declared within a method—a feature introduced in C# 7.

get
{
   double Square(double d) => Pow(d, 2);
   return PI * Square(Radius);
}

Both lambda expressions and local functions can refer to variables within the enclosing scope for this reason, the compiler actually generates a class for each local function. To mitigate the possible performance impact, if a local function does not need to access variables from the enclosing scope, as is the case in this example, C# 8 allows you to declare the local function static like so:

static double Square(double d) => Pow(d, 2);

If you refer to a variable in the enclosing scope from within a local function that is marked as static, you’ll get a compiler error.

1.2.3 Language support for tuples

C# 7 introduced new lightweight syntax for creating and consuming tuples, similar to the syntax found in many other languages. This was the most important feature introduced in C# 7.1

How are tuples useful in practice, and why are they relevant to FP? In FP, we tend to break tasks down into small functions. You may end up with a data type whose only purpose is to capture the information returned by one function, and that’s expected as input by another function. It’s impractical to define dedicated types for such structures, which don’t correspond to meaningful domain abstractions. That’s where tuples come in.

Let’s look at an example. Imagine you have a currency-pair identifier such as EURUSD, which identifies the exchange rate for Euros/US Dollars, and you’d like to break it up into its two parts:

  • The base currency (EUR)

  • The quote currency (USD)

For this, you can define a general function that splits a string at the given index. The following example shows this operation:

public static (string, string)                    
   SplitAt(this string s, int at)
   => (s.Substring(0, at), s.Substring(at));      
 
var (baseCcy, quoteCcy) = "EURUSD".SplitAt(3);    
baseCcy  // => "EUR"
quoteCcy // => "USD"

Declares a tuple as the method’s return type

Constructs a tuple

Deconstructs a tuple

Furthermore, you can assign meaningful names to the elements of a tuple. This allows you to query them like properties:

public static (string Base, string Quote)    
   AsPair(this string ccyPair)
   => ccyPair.SplitAt(3);
 
var pair = "EURUSD".AsPair();
pair.Base  // => "EUR"                       
pair.Quote // => "USD"                       

Assigns names to the elements of the returned tuple

Accesses the elements by name

Let’s see another example. You know you can use Where with a predicate to filter the values in a list:

var nums = Enumerable.Range(0, 10);
var even = nums.Where(i => i % 2 == 0);
 
even // => [0, 2, 4, 6, 8]

What if you want to know both the elements that satisfy the predicate and those that don’t, in order to process them separately? For this, I’ve defined a method called Partition, which returns a tuple containing both lists:

var (even, odd) = nums.Partition(i => i % 2 == 0);
 
even // => [0, 2, 4, 6, 8]
odd  // => [1, 3, 5, 7, 9]

As these examples illustrate, tuple syntax allows you to elegantly write and consume methods that need to return more than one value. There’s no good reason to define a dedicated type to hold together those values.

1.2.4 Pattern matching and record types

Versions 8 and 9 of C#, which appeared after the first edition of this book was published, bring us two important features that are directly inspired by functional languages:

  • Pattern matching—Lets you use the switch keyword to match not only on specific values but also on the shape of the data, most importantly its type

  • Records—Boilerplate-free immutable types with built-in support for creating modified versions

TIP The appendix shows you how you can work with pattern matching and immutable types if you’re working with legacy code and are stuck with an older version of C#.

I’ll illustrate how you can use these through a practical example. If you’ve ever worked in e-commerce, you may have come across the need to evaluate the value-added tax (VAT) that your customers will pay with their purchases.2

Imagine you’re tasked with writing a function that estimates the VAT a customer needs to pay on an order. The logic and amount of VAT depends on the country to which the item is sent and, of course, on the purchase amount. Therefore, we’re looking to implement a function named Vat that will compute a decimal (the tax amount), given an Order and the buyer’s Address. Assume that the requirements are as follows:

  • For goods shipped to Italy and Japan, VAT will be charged at a fixed rate of 22% and 8%, respectively.

  • Germany charges 8% on food products and 20% on all other products.

  • The US charges a fixed rate on all products, but the rate varies for each state.

Before you read on, you might like to take a moment to think how you would go about tackling this task.

The following listing shows how you can use record types to model an Order. To keep things simple, I’m assuming that an Order cannot contain different types of Products.

Listing 1.6 Positional records

record Product(string Name, decimal Price, bool IsFood);    
 
record Order(Product Product, int Quantity)                 
{
   public decimal NetPrice => Product.Price * Quantity;
}

A record without a body ends with a semicolon.

A record can have a body with additional members.

Notice how with a single line, you can define the Product type! The compiler generates a constructor, property getters, and several convenience methods such as Equals, GetHashCode, and ToString for you.

NOTE Records in C# 9 are reference types, but C# 10 allows you to use record syntax to define value types by simply writing record struct rather than just record. Somewhat surprisingly, record structs are mutable, and you have to declare your struct as readonly record struct if you want it to be immutable.

The following listing shows how we can implement the first business rule, which applies to countries with a fixed VAT rate, like Italy and Japan.

Listing 1.7 Pattern matching on a value

static decimal Vat(Address address, Order order)
   => Vat(RateByCountry(address.Country), order);
 
static decimal RateByCountry(string country)
   => country switch
   {
      "it" => 0.22m,
      "jp" => 0.08m,
      _ => throw new ArgumentException($"Missing rate for {country}")
   };
 
static decimal Vat(decimal rate, Order order)
   => order.NetPrice * rate;

Here I’ve defined RateByCountry to map country codes to their respective VAT rates. Notice the clean syntax of a switch expression compared to the traditional switch statement with its clunky use of case, break, and return. Here we simply match on the value of country.

Also notice that the code in listing 1.7 assumes there is an Address type with a Country property. This can be defined as follows:

record Address(string Country);

What about the other fields that make up an address, like the street, postal code, and so on? No, I didn’t forget or leave them out for simplicity. Because we only require the country for this calculation, it’s legitimate to have an Address type that only encapsulates the information we need in this context. You could have a different, richer definition of Address in a different component, and define a conversion between the two, if required.

Let’s move on and add the implementation for goods shipped to Germany. As a reminder, Germany charges 8% on food products and 20% on all other products. The code in the following listing shows how we can add this rule.

Listing 1.8 Deconstructing a record in a pattern-matching expression

static decimal Vat(Address address, Order order)
   => address switch
   {
      Address("de") => DeVat(order),
      Address(var country) => Vat(RateByCountry(country), order),
   };
 
static decimal DeVat(Order order)
   => order.NetPrice * (order.Product.IsFood ? 0.08m : 0.2m);

We’ve now added a switch expression within the Vat function. In each case, the given Address is deconstructed, allowing us to match on the value of its Country. In the first case, we match it against the literal value "de"; if this matches, we call the VAT computation for Germany, DeVat. In the second case, the value is assigned to the country variable and we retrieve the rate by country as previously. Note that it’s possible to simplify the clauses of the switch expression as follows:

static decimal Vat(Address address, Order order)
   => address switch
   {
      ("de") _ => DeVat(order),
      (var country) _ => Vat(RateByCountry(country), order),
   };

Because the type of address is known to be Address, you can omit the type. In this example, you must include a variable name for the matching expression; here we use a discard, the underscore character. This is not required if the object being deconstructed has at least two fields.3

Property patterns

The previous listing shows how to match on the value of a field by deconstructing the Address; this is called a positional pattern. Now, imagine that your Address type were more complex, including half a dozen fields or so. In this case, a positional pattern would be noisy, as you’d need to include a variable name (at least a discard) for each field.

This is where property patterns are better suited. The following code shows how you can match on the value of a property:

static decimal Vat(Address address, Order order) 
   => address switch 
   { 
      { Country: "de" } => DeVat(order), 
      { Country: var c } => Vat(RateByCountry(c), order), 
   }; 

This syntax offers the advantage that you do not need to change anything if you later add an extra field to Address. In general, property patterns work best with your typical OO entities, whereas positional patterns work best with very simple objects whose definition is unlikely to change (like a 2D point), or with objects that were modeled around a specific pattern matching scenario, like the simplified Address type in the current example.

Now for the US. Here we also need to know the state to which the order is going because different states apply different rates. You can model this as follows:

record Address(string Country);
record UsAddress(string State) : Address("us");

That is, we create a dedicated type to represent a US address. This extends Address because it has additional data. (In my opinion, this is better than adding a State property to Address and having it be null for the majority of countries.) We can now complete our requirements as the following listing shows.

Listing 1.9 Pattern matching by type

static decimal Vat(Address address, Order order)
   => address switch
   {
      UsAddress(var state) => Vat(RateByState(state), order),
      ("de") _ => DeVat(order),
      (var country) _ => Vat(RateByCountry(country), order),
   };
 
static decimal RateByState(string state)
   => state switch
   {
      "ca" => 0.1m,
      "ma" => 0.0625m,
      "ny" => 0.085m,
      _ => throw new ArgumentException($"Missing rate for {state}")
   };

RateByState is implemented along the same lines as RateByCountry. What’s more interesting is the pattern matching in Vat. We can now match on the UsAddress type, extracting the state, to find the rate applicable to that state.

TIP This section illustrates the most common (and most useful) patterns that C# supports. In addition, you can use relational patterns to match, say, all values greater than 100, or logical patterns to combine several other patterns. Head to https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns for the full specification.

And we’re done! The whole thing is just over 40 lines, most functions are one-liners, and the three cases in our requirements are clearly expressed in the corresponding cases in the top-level switch expression. We didn’t need to go crazy with functions (yet). We didn’t need to create an interface with multiple implementations as an OO programmer (seeing this problem as a perfect candidate for the strategy pattern) probably would have. Instead, we just used a type-driven approach that is representative of how records and pattern matching can be used in statically-typed functional languages.

The resulting code is not only concise, but is also readable and extensible. You can see that it would be easy for any programmer to come in and add new rules for other countries or modify the existing rules if required.

1.3 What you will learn in this book

In this chapter, you’ve seen some of the basic ideas of FP and the C# features that allow you to program in a functional style. This book does not assume any prior knowledge of functional programming. It does assume you know .NET and C# well (or, alternatively, a similar language like Java, Swift, or Kotlin). This book is about functional programming, not C#. After reading this book, you will be able to

  • Use higher-order functions to achieve more with less code and reduce duplication

  • Use pure functions to write code that is easy to test and optimize

  • Write APIs that are pleasant to consume and accurately describe your program’s behavior

  • Use dedicated types to handle nullability, system errors, and validation rules in a way that’s elegant and predictable

  • Write testable, modular code that can be composed without the overhead of an IoC container

  • Write a Web API in a functional style

  • Write complex programs with simple, declarative code, using high-level functions to process elements in a sequence or a stream of values

  • Read and understand literature written for functional languages

Summary

  • Functional programming (FP) is a powerful paradigm that can help you make your code more concise, maintainable, expressive, robust, testable, and concurrency-friendly.

  • FP differs from object-oriented programming (OOP) by focusing on functions rather than objects, and on data transformations rather than state mutations.

  • FP can be seen as a collection of techniques that are based on two fundamental tenets:

    • Functions are first-class values.
    • In-place updates should be avoided.
  • C# is a multi-paradigm language that has steadily incorporated functional features, allowing you to reap the benefits of programming in a functional style.


1 C# 7 tuples supersede their clunky C# 4 predecessors, which were suboptimal in performance and unattractive in syntax, their elements being accessed via properties called Item1, Item2, and so on. Apart from the new syntax, the underlying implementation of tuples also changed. The old tuples are backed by the System.Tuple classes, which are immutable reference types. The new tuples are backed by the System .ValueTuple structs. Being structs, they’re copied when passed between functions, yet they’re mutable, so you can update their members within methods, which is a compromise between the expected immutability of tuples and performance considerations.

2 VAT is also called sales tax or consumption tax, depending on the country you’re in.

3 The problem is that in C# ("de") is identical to "de", so the compiler would think you’re matching on a string, rather than an object with a single string field.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.76.154