Getting started with functional programming in C#
Functional programming is a programming paradigm – a different style of building programs than the imperative. In FP there are two fundamental concepts:
- Functions as first-class values
- Avoiding changing state and mutable data
First-class functions
C# treats functions as first-class citizen. You can use them as inputs or outputs of other functions, assign them to variables or store them in collections. Basically, you can do all the operations that you can do with values of other type. For example:
// assign function to variable isEven
Func<int, bool> isEven = x => x % 2 == 0;
var list = Enumerable.Range(1, 10);
// pass isEven as an argument to Where
var even = list.Where(isEven);
In the example you assign the function x => x % 2 == 0 to a variable isEven. Then you pass this variable to Where. This creates a new IEnumerable containing the elements after applying the isEven function.
Avoiding changing state
This means that once created, an object never changes, and variables should not be reassigned. The idea of not changing the state of a variable once it is been created is called immutability. Here is C#’s greatest shortcoming – everything is mutable by default. This is in contrast to most functional languages like F# where variables are immutable by default.
int[] original = { 1, 7, 4, 3 };
var sorted = original.OrderBy(x => x);
var filtered = original.Where(x => x % 2 == 0);
In this example the original array is not affected by the sorting or filtering. This is because OrderBy and Where returned new IEnumerables. A nonfunctional approach is to sort the list in place:
var original = new List<int>() { 1, 7, 4, 3 };
original.Sort();
After the sorting, the original list is changed. This is problematic and could lead to unpredictable results in multithreaded environment:
var numbers = Enumerable.Range(-10000, 20001).ToList();
Action task1 = () => Console.WriteLine(numbers.Sum());
Action task2 = () => { numbers.Sort(); Console.WriteLine(numbers.Sum()); };
Parallel.Invoke(task1, task2);
// possible output:
// -623
// 0
Here task1 comes up with an incorrect result. This is because while task1 reads the numbers to compute the sum, task2 is changing that same list. We can fix this by using Linq’s OrderBy method:
Action task3 = () => Console.WriteLine(numbers.OrderBy(x => x).Sum());
Parallel.Invoke(task1, task3);
// output:
// 0
// 0
Now we get the correct result. This is because task3 is not changing the original list.
Resources: