In this article, I discuss IResult, which I created based on the concept of an Option (or Maybe) type from functional programming languages. IResult utilizes some of these new features of C# 7.0 to emulate an the Option type from F#, including helper functions like Bind, Map and Fold. I look at reading input, parsing input, IResult extension methods, and how to put it all together.
Introduction
The features introduced in C# 7.0 make it just a little easier to introduce some functional-programming style patterns into enterprise C# code. One of the features that had me particularly excited was pattern matching, particularly in switch
blocks.
IResult
utilizes some of these new features to emulate an the Option type from F#, including helper functions like Bind
, Map
and Fold
.
Pet Peeves: Null and Exceptions
There are two things that drive me batty when it comes to OOP: exceptions and null values.
If I attempt to get a Person
object and print out their name to the console, the call should look like this:
public void PrintPersonName(int personId)
{
Person person = DBStuff.GetPersonById(personId);
Console.WriteLine($"{person.FirstName} {person.LastName}");
}
So far so good, right? Well, there are three things that can happen at this point.
- I get a fully hydrated
Person
object in my person
variable. (This is the best case scenario.) - I get a
Person
object back, but for whatever reason it wasn’t hydrated. Maybe the id doesn’t exist? I don’t know. Either way, my person
variable is now null
. - The database is locked up, or a squirrel chewed through the T1 lines and I have no connection to the network.
DBStuff.GetPerson()
doesn’t (and shouldn’t) handle this, so it throws an exception.
By my calculations, that gives me 1:3 odds of getting an actual Person
object back from a method whose sole purpose is to return a Person
object.
If I want to be safe about getting this Person
object, I need to catch any exceptions that could be thrown. I also might want to log any errors for easier troubleshooting down the road. The refactored code would look something like this:
public void PrintPersonName(int personId)
{
Person person = SafeGetPerson(personId);
if (person != null)
{
Console.WriteLine($"{person.FirstName} {person.LastName}");
}
else
{
Console.WriteLine("Person not found.");
}
}
public Person SafeGetPerson(int personId)
{
try
{
return DBStuff.GetPersonById(personId);
}
catch (Exception e)
{
LogException(e);
return null;
}
}
Now we’ve covered all of our bases, but now our code is ugly as sin, and difficult to read. If you’ve worked on enterprise code for any length of time, you know how out of control this will get.
Introducing IResult
In order to solve this problem, I’ve created IResult<T>
, which is based on the concept of an Option (or Maybe) type from functional programming languages.
IResult<T>
comes in three flavors that correspond to the three possible results I discussed above:
ISuccessResult<T>
represents a successful operation. It has a single, non-null property, T Value
, which contains the desired object. INoneResult
represents, well, a “none” operation. INoneResult
doesn’t have any properties, nor is it typed. IFailureResult
is an INoneResult
, with one added bonus. It has a non-null Exception
property that is populated when it’s created.
The actual implementations of the various flavors of IResult
are abstracted away, so the only way to create an IResult
is by using the methods in the static Result
class: Result.Return<T>()
and Result.Wrap<T>()
.
Result.Return<T>()
Result.Return<T>()
elevates whatever is passed to it the appropriate IResult<T>
.
Let’s refactor our PrintPersonName()
method from above using Result.Return<T>()
:
public void PrintPersonName(int personId)
{
IResult<Person> personResult = SafeGetPerson(personId);
switch (personResult)
{
case ISuccessResult<Person> success:
Console.WriteLine($"{success.Value.FirstName} {success.Value.LastName}");
return;
case IFailureResult failure:
Console.WriteLine($"Database Error: {failure.Exception.Message}");
return;
default:
Console.WriteLine("Person Not Found.");
return;
}
}
public IResult<Person> SafeGetPerson(int personId)
{
try
{
return Result.Return(DBStuff.GetPersonById(personId));
}
catch (Exception e)
{
LogException(e);
return Result.Return<Person>(e);
}
}
Utilizing the pattern matching introduced in C# 7.0, we can use a switch
statement to handle each possible case.
This isn’t the best example, though. The try
/catch
is still clunking around, and you can imagine every other try/catch in the application has that LogException()
call in it. (Talk about violating DRY!)
Result.Wrap<T>() and Result.SetLogger()
Simply put, Result.Wrap<T>()
takes a Func<T>
or Action
and executes it inside a try
/catch
. A second, optional argument is a Func<Exception, Exception>
exception handler, which allows the caller to customize how Result.Wrap<T>()
handles any caught exceptions.
Result.SetLogger()
takes an Action<Exception>
, which will fire any time an IFailureResult
is created.
Let’s refactor our code with these new tools:
public void PrintPersonName(int personId)
{
Result.SetLogger(LogException);
IResult<Person> personResult = SafeGetPerson(personId);
if (personResult is ISuccessResult<Person> success)
{
Console.WriteLine($"{success.Value.FirstName} {success.Value.LastName}");
}
else
{
Console.WriteLine("Person Not Found.");
}
}
public IResult<Person> SafeGetPerson(int personId)
{
return Result.Wrap(() => DBStuff.GetPersonById(personId));
}
Writing a UselessNamePrinter with IResult
Now that we know how to use Result.Return<T>()
and Result.Wrap<T>()
, let’s write a little console application that allows a user to enter a Person
Id to view that Person
’s First and Last name. We’ll write it both with and without IResult
so we can compare the two. Let’s define some quick specs.
The application will:
- Only display active
Person
records - Validate the input so only non-negative integers will be looked up
- Log any thrown exceptions to the Console (exception message) and Debug (stack trace)
A Person
record contains:
Id (int)
FirstName (string)
LastName (string)
IsActive (bool)
Reading Input
Let’s start with reading user input. For our OOP version, this is pretty simple.
private static string ReadInput()
{
Console.Write("Enter Active Person Id: ");
return Console.ReadLine();
}
Now for our IResult
version:
private static IResult<string> ReadInput()
{
Console.Write("Enter Active Person Id: ");
return Result.Wrap(Console.ReadLine);
}
Parsing Input
Now let’s parse that input into an integer. For the OOP version, I’m going to use a try pattern, and since we said we want to log invalid input, I used int.Parse()
instead of int.TryParse()
.
private static bool TryParseInput(string input, out int value)
{
try
{
value = int.Parse(input);
return true;
}
catch (Exception e)
{
LogException(new Exception("Invalid Input.", e));
value = -1;
return false;
}
}
Now our IResult
version. I used Result.Wrap<T>()
and included an exception handler to match our OOP version. Don’t worry, we’ll cover the logging in a bit.
private static IResult<int> ParseInput(string input)
{
return Result.Wrap(() => int.Parse(input), e => new Exception("Invalid Input.", e));
}
Retrieving and Printing a Person
For our OOP version, this should look pretty familiar from our earlier examples, with the added check for IsActive
.
private static string GetMessage(int personId)
{
var person = SafeGetPerson(personId);
return person != null && person.IsActive
? $"Person Id {person.Id}: {person.FirstName} {person.LastName}"
: "Person Not Found.";
}
private static Person SafeGetPerson(int personId)
{
try
{
return DBStuff.GetPersonById(personId);
}
catch (Exception e)
{
LogException(e);
return null;
}
}
Introducing the IResult Extension Methods
There are several extension methods for IResult
objects that may seem foreign to you at first, if you’ve never written in a functional language. Don’t worry! If you’ve used LINQ, you’ve probably used these functions before! Many of LINQ’s functions are actually direct ports from FP, but have been renamed to better relate to developers who are used to querying data using SQL.
Filter and Fold
Filter
is similar to LINQ’s Where
method. Filter
applies a predicate to the value of an IResult
, if it’s an ISuccessResult
. If the predicate returns true
, it continues to exist as an ISuccessResult
. Otherwise, it becomes an INoneResult
.
Fold
is similar to LINQ’s Aggregate
method. It takes a folder function (Func<TOut, T, TOut>
) and an original state argument of type TOut
. If the IResult
is INoneResult
, it returns the original state, otherwise it returns the result of the folder function.
private static string GetMessage(int personId)
{
return SafeGetPerson(personId)
.Filter(p => p.IsActive)
.Fold((initialState, person) =>
$"Person Id {person.Id}: {person.FirstName} {person.LastName}",
"Person Not Found");
}
private static IResult<Person> SafeGetPerson(int personId)
{
return Result.Wrap(() => DBStuff.GetPersonById(personId));
}
Filter
is pretty straight forward, but Fold
can be a bit confusing. I mentioned above that Fold
is similar to Aggregate
, but since there's only ever a single value in our result, initialState
is always going to be whatever value is set as the second parameter of Fold
.
.Fold(
(initialState, person)
=> $"Person Id {person.Id}: {person.FirstName} {person.LastName}",
"Person Not Found");
In order to signal my intent to ignore a parameter in a lambda function, I'll put in an underscore.
private static string GetMessage(int personId)
{
return SafeGetPerson(personId)
.Filter(p => p.IsActive)
.Fold((_, person) => $"Person Id {person.Id}: {person.FirstName} {person.LastName}",
"Person Not Found");
}
Putting It All Together
We have all the parts we need, now let's put it together in our Main
method. First, let’s cover the OOP version.
public static void Main(string[] args)
{
do
{
var rawInput = ReadInput();
if (!TryParseInput(rawInput, out int parseResult)) continue;
var personId = Math.Abs(parseResult);
var message = GetMessage(personId);
Console.WriteLine(message);
} while (ReadContinue());
}
Bind, Map and Iter
Bind
is similar to LINQ’s SelectMany
. Map
is similar to LINQ’s Select
. Iter
is the same as LINQ ForEach
.
public static void Main(string[] args)
{
Result.SetLogger(LogException);
do
{
ReadInput()
.Bind(ParseInput)
.Map(Math.Abs)
.Map(GetMessage)
.Iter(Console.WriteLine);
} while (ReadContinue());
}
Let’s break this down a little bit.
ReadInput()
.Bind(ParseInput)
We already know that ReadInput
returns an IResult<string>
. That is fed into our next method, Bind
, which also takes a function with the signature Func<int, IResult<string>>
. If the result of ReadInput
is ISuccessResult
, it supplies ParseInput
with the value and returns the result. If ReadInput
isn’t a success, ParseInput
is never executed.
.Map(Math.Abs)
.Map(GetMessage)
Map
is similar to bind, but the function it takes as a parameter doesn’t return an IResult. Internally, Map
uses Result.Wrap
to execute the function passed, so there isn’t any risk of exceptions being thrown here either.
.Iter(Console.WriteLine);
Iter
works much the same way as Bind
and Map
, but with an Action instead of a function. Since Iter
returns void, this is the end of the road.
And that’s it! Let’s see the classes in full so we can get the whole picture.
Full Class Example: OOP
internal static class Program
{
public static void Main(string[] args)
{
do
{
var rawInput = ReadInput();
if (!TryParseInput(rawInput, out int parseResult)) continue;
var personId = Math.Abs(parseResult);
var message = GetMessage(personId);
Console.WriteLine(message);
} while (ReadContinue());
}
private static string ReadInput()
{
Console.Write("Enter Active Person Id: ");
return Console.ReadLine();
}
private static bool TryParseInput(string input, out int value)
{
try
{
value = int.Parse(input);
return true;
}
catch (Exception e)
{
LogException(new Exception("Invalid Input.", e));
value = -1;
return false;
}
}
private static string GetMessage(int personId)
{
var person = SafeGetPerson(personId);
return person != null && person.IsActive
? $"Person Id {person.Id}: {person.FirstName} {person.LastName}"
: "Person Not Found.";
}
private static Person SafeGetPerson(int personId)
{
try
{
return DBStuff.GetPersonById(personId);
}
catch (Exception e)
{
LogException(e);
return null;
}
}
private static bool ReadContinue()
{
Console.WriteLine();
Console.WriteLine("Press <Esc> to exit, any other key to continue...");
Console.WriteLine();
return Console.ReadKey(true).Key != ConsoleKey.Escape;
}
private static void LogException(Exception e)
{
Console.WriteLine(e.Message);
Debug.WriteLine(e);
}
}
Full Class Example: Result
internal static class Program
{
public static void Main(string[] args)
{
Result.SetLogger(LogException);
do
{
ReadInput()
.Bind(ParseInput)
.Map(Math.Abs)
.Map(GetMessage)
.Iter(Console.WriteLine);
} while (ReadContinue());
}
private static IResult<string> ReadInput()
{
Console.Write("Enter Active Person Id: ");
return Result.Wrap(Console.ReadLine);
}
private static IResult<int> ParseInput(string input)
{
return Result.Wrap(() => int.Parse(input), e => new Exception("Invalid Input.", e));
}
private static string GetMessage(int personId)
{
return SafeGetPerson(personId)
.Filter(p => p.IsActive)
.Fold((_, person) =>
$"Person Id {person.Id}: {person.FirstName} {person.LastName}",
"Person Not Found");
}
private static IResult<Person> SafeGetPerson(int personId)
{
return Result.Wrap(() => DBStuff.GetPersonById(personId));
}
private static bool ReadContinue()
{
Console.WriteLine();
Console.WriteLine("Press <Esc> to exit, any other key to continue...");
Console.WriteLine();
return Console.ReadKey(true).Key != ConsoleKey.Escape;
}
private static void LogException(Exception e)
{
Console.WriteLine(e.Message);
Debug.WriteLine(e);
}
}
Conclusion
This application is by no means a perfect example, but it is very illustrative.
On closer inspection, the Result version of our application has no variable declarations, no switch
or if
statements, no try
/catch
blocks and only a single control flow statement: the do
-while
block in the Main
method. It’s also 20 lines shorter than the OOP version, and while line count isn’t always a great indicator, less code generally means easier maintenance.
References
If you want a more in-depth discussion of Option types, check out Scott Wlaschin's fantastic site, F# for fun and profit. I learned everything I know from his writing there. Understanding F# Types: The Option type.
Also check out:
History
- 9th June, 2017: Initial version