Embracing functional programming in C# - Part 2

In this post, we will explore the concept of functional programming and elucidate why, in certain scenarios, it proves more fitting than imperative programming.

What is functional programming ?

Functional programming is frequently touted as the panacea for addressing all bugs in programs; it facilitates readability, testability, and maintainability. While this holds true in specific contexts, it's essential to delve into the rationale behind the commercial pitch.

What leads to bugs in software ?

It is challenging to answer this question succinctly. Bugs can be triggered by transient failures, unresponsive services, non-scalable components, or other availability and performance issues. In fact, we are referring here to intrinsic bugs provoked by a flawed design in the code. Among them, one of the more problematic issues is state mutation.

  • State mutation refers to the process of modifying the state of an object or variable. It involves changing the value or properties of the object, often leading to side effects that can impact the behavior of the program.

  • State mutation is prevalent in imperative programming paradigms, where the focus is on describing step-by-step procedures and manipulating the state of variables.

The challenge with state mutation is that it introduces complexity and makes it harder to reason about the behavior of a program. Unintended consequences can arise when different parts of the codebase modify the same state, leading to bugs that are difficult to trace and debug.

Example 1

Consider the following C# code that calculates the sum of integers in a list.

1public static int Sum(List<int> list) {
2    var sum = 0;
3    foreach (var x in list)
4        sum += x;
5    return sum;
6}

There is nothing incorrect in this code, and now, imagine that we need a method that calculates the sum of absolute values. To prevent duplication, we reuse the previous function.

1public static int SumAbsolute(List<int> list) {    
2    for (var i = 0; i < list.Count(); ++i)
3        list[i] =  Math.Abs(list[i]);
4    return Sum(list);
5}

Now we can use these methods in the main program.

1static void Main(string[] args)
2{
3    var data = new List<int>() { -1, 0 };
4    Console.WriteLine($"Sum of absolute values: {SumAbsolute(data)}");
5    Console.WriteLine($"Sum of values: {Sum(data)}");
6}

The core issue lies here because the SumAbsolute method directly mutates the input list without copying its values beforehand. In our context, detecting the problem was relatively straightforward, but consider the potential complications in a codebase comprising several thousand lines.

Example 2

This example is extracted from Functional Programming in C# (Buonanno).

 1public class Product
 2{
 3    int inventory;
 4	
 5    public bool IsLowOnInventory { get; private set; }
 6    public int Inventory
 7    {
 8        get { return inventory; }
 9        private set
10        {
11            inventory = value;
12            // At this point, the object can be in an invalid state, from the perspective of any thread reading its properties.
13            IsLowOnInventory = inventory <= 5;
14        }
15    }
16}

In a multithreaded setting, there exists a brief window wherein the Inventory has been updated, but IsLowOnInventory has not. It occurs very infrequently, but it is possible, and during such instances, debugging becomes exceptionally challenging. These troublesome bugs are known as race conditions and are incredibly challenging to detect, if detection is even possible. While one might assume that our code is often single-threaded, unfortunately, multicore processors are making concurrency more and more prevalent.

Indeed, the move toward multicore machines is one of the main reasons for the renewed interest we're currently seeing in FP.
Functional Programming in C# (Buonanno)

In contrast, immutability, where once an object is created, its state cannot be altered, helps in creating more predictable and bug-resistant code. More on this later.

Why do we talk about functional programming ?

We've just observed that state mutation can lead to significant bugs in certain programs. Now, let's consider a straightforward real-valued function $f$ of a single variable such that $f(x)=x^2+x+1$. What is the outcome of $f(1)$ ?

This question appears quite straightforward, and the result is $3$. How do we calculate that ? We take 1, square it, add 1, and then add 1 again. Why all this fuss ?

The second method of calculation appears absolutely nonsensical: nevertheless, this is precisely what we occasionally engage in when altering the state of a variable throughout a computation (in our scenario, initially, x equals one, and then, in the midst of the operation, x becomes equal to 2). While it may sound trivial when articulated in this manner, it undeniably reflects the actuality of the situation.

When assessing a mathematical function for a specific value, that value remains constant throughout the computation. In fact, this aligns with one of the core concepts of functional programming and is a fundamental aspect of its name: the result will only depend on its arguments, irrespective of any global state. This concept is known as purity, and functional programming endeavors to steer clear of state mutation to maintain purity.

What is a pure function ?

A pure function is a function that, given the same input, will always produce the same output and has no observable side effects.

  • The output of the function is solely determined by its input.
  • The function does not modify any external state or variables. It doesn't rely on or change anything outside of its scope (no side effects).

Pure functions are a fundamental concept in functional programming, promoting predictability, testability, and ease of reasoning about code.

This is the reason why, in purely functional languages like F#, variables are strictly immutable and cannot be altered once initialized. In C#, immutability is not inherently guaranteed by design, and we must employ alternative methods to uphold this essential property.

Information

The recently fashioned programming language Rust is not inherently a functional language, but variables are immutable by default. This serves as an acknowledgment that the mutation of state is identified as one of the fundamental sources of bugs.

Important

Side effects are inevitable in computer science. There will always be a need to modify a database or a file; otherwise, our work would be entirely pointless. The philosophy of functional programming is simply to isolate these effects in impure functions and code everything else with pure functions.

Why purity matters ?

Purity is not merely a philosophical concept. Parallelism is significantly streamlined with pure functions, as we don't have to consider side effects or any global state. This is why functional programming is regarded as enhancing concurrency. Similarly, testing becomes notably straightforward with pure functions, thereby enhancing testability as well.

We will explore that aspect in the final post of this series.

Functional programming is connected to mathematical functions.

Functional programming is, therefore, a paradigm that avoids mutation. However, the connection with a mathematical function extends beyond this.

When defining a function $f$, we comprehend its domain $A$ (the values it can evaluate) and are aware of its potential return values $B$. This mapping is denoted as $f:A \longmapsto B$.

In programming languages, the same principle should apply: we ought to be able to predict the outcome of any function or procedure that we use. However, is this always the case ?

1int Divide(int x, int y)
2{
3    return x / y;
4}

The signature states that the function accepts two integers and returns another integer. But this is not the case in all scenarios. What happens if we invoke the function like Divide(1, 0)? The function implementation doesn't abide by its signature, throwing DivideByZeroException.
Functional Programming in C# (https://functionalprogrammingcsharp.com/honest-functions)

Important

An honest function is one that accurately conveys all information about its possible inputs and outputs, consistently honoring its signature without any hidden dependencies or side effects.

How can we make this function an honest one? We can change the type of the y parameter (NonZeroInteger is a custom type which can contain any integer except zero).

1int Divide(int x, NonZeroInteger y)
2{
3    return x / y.Value;
4}

An honest function is not merely a philosophical concept. Having precise knowledge of what a function returns allows for chaining this function, much like composing functions in a mathematical sense. Therefore, honesty, or referential transparency, enhances code predictability, testability, and reasoning.

LINQ is indeed an example in the .NET framework of code written in a functional style. LINQ allows us to chain, or compose, functions effectively.

1var res = accounts.Where(x => x.IsActive).Select(t => t.Name).OrderByDescending();

Why would one opt for functional programming with C#? Why not utilize Haskell, Erlang, or F#?

This is an excellent question. Why not program in Haskell, F#, or another programming language since they are so wonderful ? The issue is that we must consider the reality: there is a surplus of C# developers while F# engineers are quite scarce.

No company would take the risk of being unable to hire because it adopts a marvelous paradigm that nobody is proficient in (this option is, in fact, only viable for large enterprises). That's why the compromise was made to stick with C# and attempt to incorporate functional programming within this language.

Information

C# is predominantly an object-oriented language, allowing it to serve as a bridge between the two worlds.

How to implement functional programming in C#? This is the subject of the next article.

Embracing functional programming in C# - Part 3