Click here to Skip to main content
13,259,779 members (45,252 online)
Click here to Skip to main content
Add your own
alternative version

Stats

14.6K views
229 downloads
38 bookmarked
Posted 9 Jun 2017

IResult - A Robust Option Type for C#

, 9 Jun 2017
Rate this:
Please Sign up or sign in to vote.
An attempt to introduce some Functional Programming concepts into an OOP domain.

Source on Github: FunctionalOOP.Result

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.

  1. I get a fully hydrated Person object in my person variable. (This is the best case scenario.)
  2. 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.
  3. 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 who’s 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:

  1. ISuccessResult<T> represents a successful operation. It has a single, non-null property, T Value, which contains the desired object.
  2. INoneResult represents, well, a “none” operation. INoneResult doesn’t have any properties, nor is it typed.
  3. 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.

// OOP Version
private static string ReadInput()
{
    Console.Write("Enter Active Person Id: ");
    return Console.ReadLine();
}

Now for our IResult version:

// Result 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().

// OOP version
private static bool TryParseInput(string input, out int value)
{
    try
    {
        value = int.Parse(input);
        return true;
    }
    catch (Exception e)
    {
        // LogException logs to both Console and Debug
        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.

// Result version
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.

// OOP version
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 logs to both Console and Debug
        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.

// Result version
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) // initialState is "Person Not Found"
        => $"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. 

// Result version
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 lets put it together in our Main method. First, let’s cover the OOP version.

// OOP Version
public static void Main(string[] args)
{
    do
    {
        var rawInput = ReadInput(); // First we get our input
        
        // Then we parse it
        if (!TryParseInput(rawInput, out int parseResult)) continue;

        var personId = Math.Abs(parseResult); // Make sure it isn't negative
        var message = GetMessage(personId); // Get our message to the user
        Console.WriteLine(message); // Pass our message into Console.Writeline

    } 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); // Setting up our logger so all exceptions are logged accordingly
    do
    {
        ReadInput() // First we get our input
            .Bind(ParseInput) // Then we parse it
            .Map(Math.Abs) // Make sure it isn't negative
            .Map(GetMessage) // Get our message to the user
            .Iter(Console.WriteLine); // Pass our message into Console.Writeline
    } while (ReadContinue());
}

Let’s break this down a little bit.

ReadInput() // First we get our input
            .Bind(ParseInput) // Then we parse it

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) // Make sure it isn't negative
.Map(GetMessage) // Get our message to the user

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); // Pass our message into 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

// OOP Version
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:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jeremy Madden
Software Developer
United States United States
I started my dev career writing in a custom port of PL/1 for 4 years.

Now I'm an enterprise C# developer in Portland, OR.

I think I'll stick with C#.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
tbayart25-Jul-17 2:02
membertbayart25-Jul-17 2:02 
GeneralMy vote of 5 Pin
Philip Shaffer24-Jul-17 9:14
memberPhilip Shaffer24-Jul-17 9:14 
QuestionGreat Article Pin
Rob Grainger17-Jul-17 2:47
memberRob Grainger17-Jul-17 2:47 
GeneralMy vote of 5 Pin
DrABELL9-Jul-17 7:23
professionalDrABELL9-Jul-17 7:23 
GeneralMy vote of 5 Pin
D V L8-Jul-17 7:22
professionalD V L8-Jul-17 7:22 
GeneralMy vote of 5 Pin
Ehsan Sajjad23-Jun-17 4:15
professionalEhsan Sajjad23-Jun-17 4:15 
Questionan excellent article, thanks, and one comment Pin
BillWoodruff19-Jun-17 6:06
mvpBillWoodruff19-Jun-17 6:06 
AnswerRe: an excellent article, thanks, and one comment Pin
Jeremy Madden17-Jul-17 5:33
professionalJeremy Madden17-Jul-17 5:33 
QuestionI like this. Pin
Pete O'Hanlon12-Jun-17 3:35
protectorPete O'Hanlon12-Jun-17 3:35 
QuestionI like it, this is also a good Option C# class Pin
Sacha Barber11-Jun-17 1:41
mvpSacha Barber11-Jun-17 1:41 
QuestionThis reminds me of Ether.Outcomes Pin
rosdi10-Jun-17 0:24
memberrosdi10-Jun-17 0:24 
QuestionGreat minds think alike! Pin
onelopez9-Jun-17 17:54
memberonelopez9-Jun-17 17:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 9 Jun 2017
Article Copyright 2017 by Jeremy Madden
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid