Imagine that you want to write a simple console application that queries the user for two integers, divides the first integer with the second, and writes the result to the console window. What can go wrong in a simple program like this? The first thing that comes to mind is probably that the user might enter something other than integers, like ”rainbow unicorns” or ”l33t h4x0r”. Secondly, he might enter a zero as the second integer, trying to crash the program by having it perform a division by zero (throws a DivideByZeroException).
In order to handle these cases you can use the Int32.TryParse method for parsing the input and a try-catch statement to cover a possible division by zero. Your code might look something like the following:
static void Main()
{
var numerator = QueryUser("Numerator: ");
var denominator = QueryUser("Denominator: ");
if (int.TryParse(numerator, out var n) &&
int.TryParse(denominator, out var d))
{
try
{
WriteLine($"{numerator} / {denominator} = {n / d}");
}
catch (Exception e)
{
WriteLine($"An error occurred: {e.Message}");
}
}
else
{
WriteLine("Only integers are allowed as input.");
}
}
In the code above the QueryUser method is a simple helper method that writes a message to the console and reads the user’s response. The implementation follows the standard imperative pattern using if-statements for controlling the program flow and the standard OOP error handling using try-catch statements.
Now, let me present to you a different approach for handling lack of values and possible errors. It involves using the C# Functional Programming Language Extensions, language-ext, by Paul Louth.
static void Main()
{
var numerator = QueryUser("Numerator: ");
var denominator = QueryUser("Denominator: ");
var result =
from n in Some(numerator).Bind(parseInt)
from d in Some(denominator).Bind(parseInt)
select Divide(n, d);
result.Match(
Some: s => s.Match(
Succ: res => WriteLine($"{numerator} / {denominator} = {res}"),
Fail: err => WriteLine($"An error occurred: {err.Message}")),
None: () => WriteLine("Only integers are allowed as input."));
}
If you haven’t seen this approach to error handling before it might be difficult to understand what’s going on here. Where is the if-statement and the try-catch block? And what is Some, Bind, and Match?
Explaining everything in detail would require more than what is possible in a single blog post, and if you are interested in the details I recommend reading Paul Louths presentation of language-ext and also the great book Functional Programming in C# which explains all the details of the Option- and Either monads and how Monadic Bind is used to chain monadic returning functions together.
However, to understand the code above you need to know that QueryUser has not been changed, it will still return whatever the user enters, which might not be possible to parse as an integer. Some is a function that is defined in language-ext. It takes a value, in this case of type string, and lifts it into an Option-type. An Option is a container that can either contain a value, Some, or be empty, None. In this case the Option-container will have a value, the string that the user entered.
Next comes Bind. What Bind does is to take the Option and apply it’s value to the function that is given as argument to Bind. In this case Bind takes the string that the user entered and calls the parseInt function with the string as parameter. Now if the user have supplied a value that can be parsed as an int then Bind will return an Option containing the resulting value. However, if the value could not be parsed then Bind will return an Option containing None. This replaces the TryParse calls in the imperative code.
Now that we have two Option variables, n and d, we use select with the Divide function to calculate the result. It is not visible in the above code but this use of select depends on a special implementation of Select and SelectMany that accepts Option-types as input parameters (you might be familiar with the Select and SelectMany implementations for IEnumerable that is part of LINQ, these are similar but for Option).
The Divide function is an implementation of the Try delegate of language-ext which handles any possible exceptions that might occur and wraps them in a Result type. This might be a lot to wrap your head around, but if you’ve gotten this far you might be wondering about the Match functions at the end.
So, the type of the result variable is Option<Result<int>> where the outer type, Option is Some iff both the input strings were successfully parsed as int values (otherwise None), and the Result is Succ (success) iff no exception were thrown when performing the division (otherwise Fail). Match is used to check which of these four possible cases the result ended up as and outputs different texts depending on the outcome.
There are quite a lot of new concepts to understand and a very different way of thinking about errors if you want to use this approach to error handling. The small toy examples that you have to use in blog posts like this does not show any real benefits either so you might be very sceptic to all this right now. The value of the functional approach is that, once you understand the concepts, it makes your code much easier to understand and reason about. Traditional OOP and imperative code sprinkled with if-statements and exceptions being thrown and caught at different levels of the code quickly makes it extremely hard to follow. The functional approach pushes the error handling code to the end (the Match in the code above).
Finally I would like to recommend Scott Wlaschin’s introduction to Railway Oriented Programming which also includes a link to his presentation at NDC on the topic.