Embracing functional programming in C# - Part 3

In this post, we will explore referential transparency (or honesty) and examine how to implement this crucial functional concept in C#.

Information

In this series, we will utilize the LaYumba.Functional library, which is accessible as a NuGet package.

Introducing the Option concept

As developers, we routinely use common methods from the base library without questioning them. For instance, converting a string to an integer is a common operation, as shown in the following code.

1var input = Console.ReadLine();
2var s = Convert.ToInt32(input);
3Console.WriteLine(s);

This code will produce an integer output for inputs such as "1230" or "-2". However, what does the application yield when provided with an input like "hello" for instance ?

This code exemplifies a function that is not honest; neither its name nor its signature indicates that an exception can be thrown. We only anticipate the return of an integer and when a bug is eventually discovered, developers will make slight modifications to the original code.

1var input = Console.ReadLine();
2if (int.TryParse(input, out var s))
3{
4    Console.WriteLine(s);
5}
6else
7    Console.WriteLine("An error occurred.");

What is the issue with this code ?

There is nothing inherently wrong with this code. It functions as intended and helps prevent bugs. However, it is highly probable that the output value will be the input of another function (typically, code is not written solely for pleasure), and in such cases, we need to handle two branches: one for correct input and another for incorrect input. This introduces if-else statements and potentially nested if-else statements, eventually making the code unreadable and thus difficult to maintain.

Functional programming, on the contrary, suggests working with honest functions where the output is precisely known. If a method might return an exception, it must be explicitly indicated in its signature. This approach allows us to compose functions without resorting to if-else statements and is closely aligned with its mathematical counterpart. When composing multiple functions in mathematics, it is uncommon to indicate that an error can occur.

The code becomes much clearer because we can chain functions.

OK, but how can we do that ? Enter Option

In functional programming, an Option is a data type that represents the presence or absence of a value. It is a way of handling the possibility of having no result or an undefined result without resorting to using null.

  • An Option type can be either Some(value), indicating the presence of a value, or None, indicating the absence of a value.

  • The use of Option types helps in creating more honest and predictable functions, as it forces explicit handling of the absence of a value. This can lead to more robust and less error-prone code.

Information

Option is sometimes referred to as Maybe in the literature.

With this concept, we can define an honest function for converting a string to an integer.

1private static Option<int> ConvertToInt32(string input)
2{
3    return int.TryParse(input, out var s) ? Some(s) : None;
4}

The signature now clearly indicates that it is possible to not obtain data (due to an exception, for example), providing a cleaner and more explicit way to handle potential absence of values compared to using null or exceptions. The result can thus be used downstream to continue the workflow without the need for numerous if-else conditions.

1var input = Console.ReadLine();
2var res = ConvertToInt32(input);
3Console.WriteLine(res);

This coding approach allows us to chain functions naturally, resulting in a more readable code. For example, consider the following scenario

  • Read a string.
  • Convert it to an integer
  • Double its value

With an Option, the code closely resembles the previous algorithm.

1var input = Console.ReadLine();
2var res = ConvertToInt32(input).Map(x => x * 2);
3Console.WriteLine(res);

Whether an error occurs during the conversion or everything proceeds smoothly, we will obtain a result without the need for complex branching logic.

Information

For the concrete implementation of the Option concept in C#, refer to Functional Programming in C# (Buonanno).

Introducing the Either concept

The Option concept is suitable when we want to model the absence of data. However, there are scenarios where we need more information, specifically to understand why there is no value. This becomes particularly relevant when different exceptions can be thrown by the program.

Either is a data type that represents one of two possible values: Left or Right. It is often used to model a computation that can result in either a success (Right) or a failure (Left). The convention is to use Left for the error or failure case and Right for the success case. The advantage of using Either is that it provides more information about the failure by allowing us to include an error value along with the Left case. This can be helpful in situations where multiple types of errors can occur, and we want to distinguish between them.

Using Either, we can rewrite the previous code.

 1var input = Console.ReadLine();
 2var res = ConvertToInt32(input).Map(x => x * 2);
 3Console.WriteLine(res);
 4
 5private static Either<string, int> ConvertToInt32(string input)
 6{
 7    try
 8    {
 9        return Right(Convert.ToInt32(input));
10    }
11    catch (Exception ex)
12    {
13        return Left(ex.Message);
14    }
15}

Information

We only scratch the surface of the various possibilities that functional programming offers for improved readability. We cannot be exhaustive or too rigorous with the intricate details (such as monads) in this brief overview. For a deeper understanding of these concepts, it is recommended to refer to a dedicated book on the subject.

We will now explore how to approach functional thinking with data.

Embracing functional programming in C# - Part 4