FP examples
The only thing that sets FP aside from other methods of programming is that functions do not modify data or state. You will use FP in scenarios such as deep learning (DL), machine learning (ML), and artificial intelligence (AI) when it is necessary to perform different sets of operations on the same set of data.
The LINQ syntax within .NET Framework is an example of FP. So, if you are wondering what FP looks like, and if you have used LINQ before, then you have been subjected to FP and should know what it looks like.
Since FP is a deep subject and many books, courses, and videos exist on this topic, we will only touch on the topic briefly in this chapter by looking at pure functions and immutable data.
A pure function is restricted to only operating on the data that is passed into it. As a result, the method is predictable and avoids producing side effects. This benefits programmers because such methods are easier to reason about and test.
Once an immutable data object or data structure has been initialized, the contained data values will not be modified. Because the data is only set and not modified, you can easily reason about what the data is, how it is set, and what the outcome of any operation will be, given the inputs. Immutable data is also easier to test as you know what your inputs are and what outputs are expected. This makes writing test cases much easier as you don’t have so many things to consider, such as object state. The benefit of immutable objects and structures is that they are thread-safe. Thread-safe objects and structures make for good DTOs that can be passed between threads.
But structs can still be mutable if they contain reference types. One way around this would be to make the reference type immutable. C# 7.2 added support for readonly struct
and ImmutableStruct
. So, even if our structures contain reference types, we can now use these new C# 7.2 constructs to make structures with reference types immutable.
Now, let’s have a look at a pure function example. The only way to set the properties of an object is via the constructor at construction time. The class is a Player
class whose only job is to hold the name of the player and their high score. A method is provided that updates the player’s high score:
public class Player { public string PlayerName { get; } public long HighScore { get; } public Player(string playerName, long highScore) { PlayerName = playerName; HighScore = highScore; } Public Player UpdateHighScore(long highScore) { return new Player(PlayerName, highScore); } }
Notice that the UpdateHighScore
method does not update the HighScore
property. Instead, it instantiates and returns a new Player
class by passing in the PlayerName
variable, which is already set in the class, and highScore
, which is the method parameter. You have now seen a very simple example of how to program your software without changing its state.
Note
FP is a very large subject and requires a mind shift that can be very difficult for both procedural and OO programmers. Since it is outside the scope of this book (to delve deep into the topic of FP), you are actively encouraged to peruse the FP resources on offer from Packt Publishing for yourself.
Packt has some very good books and videos that specialize in teaching the top tiers of FP. You will find links to some Packt FP resources at the end of this chapter, in the Further reading section.
Before we move on, we will look at some LINQ examples since LINQ is an example of FP in C#. It will be good to have an example dataset. The following code builds a list of vendors and products. We’ll start by writing the Product
structure:
public struct Product { public string Vendor { get; } public string ProductName { get; } public Product(string vendor, string productName) { Vendor = vendor; ProductName = productName; } }
Now that we have our struct, we will add some sample data inside the GetProducts()
method:
public static List<Product> GetProducts(){ return new List<Product> { new Product("Microsoft", "Microsoft Office"), new Product("Oracle", "Oracle Database"), new Product("IBM", "IBM DB2 Express"), new Product("IBM", "IBM DB2 Express"), new Product("Microsoft", "SQL Server 2017 Express"), new Product("Microsoft", "Visual Studio 2019 Community Edition"), new Product("Oracle", "Oracle JDeveloper"), new Product("Microsoft", "Azure"), new Product("Microsoft", "Azure"), new Product("Microsoft", "Azure Stack"), new Product("Google", "Google Cloud Platform"), new Product("Amazon", "Amazon Web Services") }; }
Finally, we can start to use LINQ on our list. In the preceding example, we got a distinct list of products and ordered by the vendors’ names. Now we will print out the results:
class Program { static void Main(string[] args) { var vendors = (from p in GetProducts() select p.Vendor) .Distinct() .OrderBy(x => x); foreach(var vendor in vendors) Console.WriteLine(vendor); Console.ReadKey(); } }
In the provided C# code, the LINQ statements are used to retrieve a distinct list of vendor names from a collection of products and then order them alphabetically. The LINQ statements can be considered pure functions in the context of this code because they do not have any side effects, and their output is solely determined by their input. Here’s an explanation of how these LINQ statements act as pure functions:
from p in GetProducts() select p.Vendor
: This LINQ statement queries theGetProducts()
method to retrieve theVendor
property from each product. It transforms the input collection of products into a new sequence of vendor names. This transformation is a pure function because it does not modify the input collection or have any side effects..Distinct()
: TheDistinct()
method filters the sequence of vendor names to ensure that each vendor name appears only once in the output. This operation is also a pure function as it does not alter the original sequence and produces a new sequence based on the distinct values..OrderBy(x => x)
: TheOrderBy
method sorts the vendor names alphabetically. This sorting operation is deterministic and does not modify the input sequence but produces a new ordered sequence. It, too, is a pure function.
Overall, the LINQ statements in this code are pure functions because they take an input collection, apply various transformations to it, and produce a new collection without causing any side effects or altering the original data. This functional style of programming is one of the benefits of using LINQ in C#, as it promotes immutability and helps ensure code clarity and maintainability.
In C#, you can create an immutable type by using the record
keyword. Here’s an example of a simple immutable record a person:
public record Person{ public string FirstName { get; set; } public string LastName { get; set;} public int Age { get; set;} public Person(string firstName, string lastName, int age) { FirstName = firstName; LastName = lastName; Age = age; } }
In this example, the Person
record has three properties: FirstName
, LastName
, and Age
, and it has a constructor to initialize these properties. The properties are read-only, meaning you can’t change their values after the record is created.
Here’s how you can use the Person
record:
Person person1 = new Person("John", "Doe", 30);Person person2 = new Person("Jane", "Smith", 25); Console.WriteLine(person1.FirstName); // Output: John Console.WriteLine(person2.Age); // Output: 25
Because the Person
record is immutable, you can’t modify its properties directly. If you want to create a new Person
instance with different values, you would create a new object:
Person updatedPerson = person1 with { Age = 35 };
This creates a new Person
record with the same FirstName
and LastName
properties as person1
but with an updated Age
property.
One of the benefits of FP is that your methods are much smaller than the methods in other types of programming. Next, we will take a look at why it is good to keep methods small, as well as the techniques we can use, including FP.