Chapter 6. Discriminated Unions

Discriminated unions (DUs) are a way of defining a type (or class in the OOP world) that is actually one of a set of different types. Which type an instance of a DU actually is at any given moment has to be checked before use.

F# has DUs available natively, and it’s a feature used extensively by F# developers. Despite sharing a common runtime with C#, and the feature being there for us in theory, there are only plans in place to introduce them into C# at some point, but it’s not certain how or when. In the meantime, we can roughly simulate them with abstract classes, and that’s the technique I’m going to talk about in this chapter.

This chapter is our first dabble into some of the more advanced areas of FP. Earlier chapters were more focused on how you, the developer, can work smart, not hard. We’ve also looked at ways to reduce boilerplate and to make code more robust and maintainable.

DUs are a programming structure that will do all of this too,1 but are more than a simple extension method, or a single-line fix to remove a little bit of boilerplate. DUs are closer in concept to a design pattern—in that they have a structure and some logic that needs to be implemented around it.

Holiday Time

Let’s imagine an old-school object-oriented problem of creating a system for package holidays (or vacations in the US). You know, the sort where the travel agency arranges a customer’s travel and accommodations, all in one. I’ll leave you to imagine which lovely destination our customer is off to. Personally, I’m quite fond of the Greek islands.

Here’s a set of C# data classes that represent two different kinds of holiday—one with and one without complimentary meals provided:

public class Holiday
{
    public int Id { get; set; }
    public Location Destination { get; set; }
    public Location DepartureAirport { get; set; }
    public DateTime StartDate { get; set; }
    public int DurationOfStay { get; set; }
}

public class HolidayWithMeals : Holiday
{
    public int NumberOfMeals { get; set; }
}

Now imagine we are creating, say, an account page for our customer and want to list everything they’ve bought so far.2 That’s not all that difficult, really. We can use a relatively new is statement to build the necessary string. Here’s one way to do it:

public string formatHoliday(Holiday h) =>
    "From: " + h.DepartureAirport.Name + Environment.NewLine +
    "To: " + h.Destination.Name + Environment.NewLine +
    "Duration: " + h.DurationOfStay + " Day(s)" +
    (
        h is HolidayWithMeals hm
            ? Environment.NewLine + "Number of Meals: " + hm.NumberOfMeals
            : string.Empty
    );

If we want to quickly improve this with a few functional ideas, we could consider introducing a Fork combinator (see Chapter 5), The basic type would be Holiday, and the subtype would be HolidayWithMeals. We’d have essentially the same thing, but with an extra field or two.

Now, what if there was a project started up in the company to offer other types of services, separate from holidays. The company is going to also start providing day trips that don’t involve hotels, flights, or anything else of that sort. Entrance into Tower Bridge in London, perhaps.3 Or a quick jaunt up the Eiffel Tower in Paris. Whatever you fancy. The world is your oyster.

The object would look something like this:

public class DayTrip
{
    public int Id { get; set; }
    public DateTime DateOfTrip { get; set; }
    public Location Attraction { get; set; }
    public bool CoachTripRequired { get; set; }
}

The point is, though, that if we want to represent this new scenario with inheritance from a Holiday object, it doesn’t work. An approach I’ve seen some people follow is to merge all the fields together, along with a Boolean to indicate which fields are the ones we should be looking at:

public class CustomerOffering
{
    public int Id { get; set; }
    public Location Destination { get; set; }
    public Location DepartureAirport { get; set; }
    public DateTime StartDate { get; set; }
    public int DurationOfStay { get; set; }
    public bool CoachTripRequired { get; set; }
    public bool IsDayTrip { get; set; }
}

This is a poor idea for several reasons. For one, we’re breaking the interface segregation principle. Whichever sort of holiday an instance of CustomerOffering represents, we’re forcing it to hold fields that are irrelevant to it. We’ve also doubled up the concepts of Destination and Attraction, as well as DateOfTrip and StartDate here, to avoid duplication, but it means that we’ve lost some of the terminology that makes code dealing with day trips meaningful.

The other option is to maintain the objects as entirely separate types with no relationship between them at all. Doing that, we’d lose the ability to have a nice, concise, simple loop through every object. We wouldn’t be able to list everything in a single table in date order. We would need multiple tables.

None of the possibilities seem all that good. But this is where DUs come charging to the rescue. In the next section, I’ll show you how to use them to provide an optimum solution to this problem.

Holidays with Discriminated Unions

In F#, we can create a union type for our customer-offering example, like this:

type CustomerOffering =
    | Holiday
    | HolidayWithMeals
    | DayTrip

This means we can instantiate a new instance of CustomerOffering, but there are three separate types it could be, each potentially with its own entirely different properties.

This is the nearest we can get to this approach in C#:

public abstract class CustomerOffering
{
     public int Id { Get; set; }
}

public class Holiday : CustomerOffering
{
    public Location Destination { get; set; }
    public Location DepartureAirport { get; set; }
    public DateTime StartDate { get; set; }
    public int DurationOfStay { get; set; }
}

public class HolidayWithMeals : Holiday
{
    public int NumberOfMeals { get; set; }
}

public class DayTrip : CustomerOffering
{
    public DateTime DateOfTrip { get; set; }
    public Location Attraction { get; set; }
    public bool CoachTripRequired { get; set; }
}

On the face of it, the code doesn’t seem entirely different from the first version of this set of classes, but there’s an important difference. The base is abstract—we can’t actually create a CustomerOffering class. Instead of being a family tree of classes with one parent at the top that all others conform to, all the subclasses are different, but equal in the hierarchy.

The class hierarchy diagram in Figure 6-1 should clarify the difference between the two approaches.

The DayTrip class is in no way forced to conform to any concept that makes sense to the Holiday class. DayTrip is completely its own thing: it can use property names that correspond exactly to its own business logic, rather than having to retrofit a few of the properties from Holiday. In other words, DayTrip isn’t an extension of Holiday; it’s an alternative to it.

OO vs Discriminated Union
Figure 6-1. OOP versus DU

This also means we can have a single array of all CustomerOfferings, even though they’re wildly different. We don’t need separate data sources.

We’d handle an array of CustomerOffering objects in code by using a pattern-matching statement:

public string formatCustomerOffering(CustomerOffering c) =>
    c switch
    {
        HolidayWithMeals hm => this.formatHolidayWithMeal(hm),
        Holiday h => this.formatHoliday(h),
        DayTrip dt => this.formatDayTrip(tp)
    };

This simplifies the code everywhere the DU is received, and gives rise to more descriptive code that more accurately indicates all the possible outcomes of a function.

Schrödinger’s Union

If you want an analogy of how these DU things work, think of poor old Schrödinger’s cat. This was a thought experiment proposed by Austrian physicist Erwin Schrödinger to highlight a paradox in quantum mechanics. He imagined a box containing a cat and a radioactive isotope that had a 50-50 chance of decaying and killing the cat.4 The point was that, according to quantum physics, until someone opens the box to check on the cat, both states—alive and dead—exist at the same time (meaning that the cat is both alive and dead at the same time).5

This also means that if Herr Schrödinger were to send his cat/isotope box in the mail to a friend, they’d have a box that could contain one of two states inside, and until they open it, they don’t know which.6 Of course, the postal service being what it is, chances are the cat would be dead upon arrival either way. This is why you really shouldn’t try this one at home. Trust me (I’m not a doctor, nor do I play one on TV).

That’s kind of how a DU works. It has a single returned value, but that value may exist in two or more states. We don’t know which until we examine it. If a class doesn’t care which state, we can even pass it along to its next destination unopened.

Schrödinger’s cat as code might look like this:

public abstract class SchrödingersCat { }

public class AliveCat : SchrödingersCat { }

public class DeadCat : SchrödingersCat { }

I’m hoping you’re now clear on what exactly DUs are. I’m going to spend the rest of this chapter demonstrating a few examples of what they are for.

Naming Conventions

Let’s imagine a code module for writing out people’s names from the individual components. If you have a traditional British name, like my own, this is fairly straightforward. A class to write a name like mine would look something like this:

public class BritishName
{
    public string FirstName { get; set; }
    public IEnumerable<string> MiddleNames { get; set; }
    public string LastName { get; set; }
    public string Honorific { get; set; }
}

var simonsName = new BritishName
{
    Honorific = "Mr.",
    FirstName = "Simon",
    MiddleNames = new [] { "John" },
    LastName = "Painter
};

The code to render that name to string would be as simple as this:

public string formatName(BritishName bn) =>
    bn.Honorific + " " bn.FirstName + " " + string.Join(" ", bn.MiddleNames) +
    " " + bn.LastName;
// Results in "Mr Simon John Painter"

All done, right? Well, this works for traditional British names, but what about Chinese names? They aren’t written in the same order as British names. Chinese names are written as <family name> <given name>, and many Chinese people take a courtesy name, a Western-style name that is used professionally.

Let’s take the example of the legendary actor, director, writer, stuntman, singer, and all-round awesome human being Jackie Chan. His real name is Fang Shilong. In that set of names, his family name (surname) is Fang. His personal name (often in English called the first name, or Christian name) is Shilong. Jackie is a courtesy name he’s used since he was very young. This style of name doesn’t work whatsoever with the formatName() function we’ve created.

We could mangle the data a bit to make it work:

var jackie = new BritishName
{
    Honorific = "Xiānsheng", // equivalent of "Mr."
    FirstName = "Fang",
    LastName = "Shilong"
}
// results in "xiānsheng Fang Shilong"

So fine, this correctly writes his two official names in the required order. What about his courtesy name, though? The code provides nothing to write that out. Also, the Chinese equivalent of “Mr.”—Xiānsheng7—goes after the name, so this is really pretty shoddy, even if we try repurposing the existing fields.

We could add an awful lot of if statements into the code to check for the nationality of the person being described, but that approach would rapidly turn into a nightmare if we tried to scale it up to include more than two nationalities.

Once again, a better approach would be to use a DU to represent the radically different data structures in a form that mirrors the reality of the thing they’re trying to represent:

public abstract class Name { }

public class BritishName : Name
{
    public string FirstName { get; set; }
    public IEnumerable<string> MiddleNames { get; set; }
    public string LastName { get; set; }
    public string Honorific { get; set; }
}

public class ChineseName : Name
{
    public string FamilyName { get; set; }
    public string GivenName { get; set; }
    public string Honorific { get; set; }
    public string CourtesyName { get; set; }
}

In this imaginary scenario, there are probably separate data sources for each name type, each with its own schema. Maybe a web API for each country?

Using this union, we can create an array of names containing both me and Jackie Chan:8

var names = new Name[]
{
    new BritishName
    {
        Honorific = "Mr.",
        FirstName = "Simon",
        MiddleNames = new [] { "John" },
        LastName = "Painter"
    },
    new ChineseName
    {
        Honorific = "Xiānsheng",
        FamilyName = "Fang",
        GivenName = "Shilong",
        CourtestyName = "Jackie"
    }
}

We could then extend the formatting function with a pattern-matching expression:

public string formatName(Name n) =>
    n switch
    {
     BritishName bn => bn.Honorific + " " bn.FirstName + " "
        + string.Join(" ", bn.MiddleNames) + " " + bn.LastName,
     ChineseName cn => cn.FamilyName + " " + cn.GivenName + " " +
        cn.Honorific + " "" + cn.CourtesyName + """
    };

var output = string.Join(Environment.NewLine, names);
// output =
// Mr. Simon John Painter
// Fang Shilong Xiānsheng "Jackie"

This same principle can be applied to any style of naming for anywhere in the world, and the names given to fields will always be meaningful to that country, as well as always being correctly styled without repurposing existing fields.

Database Lookup

In my C# code, I often consider using DUs as the return type of functions. I’m especially likely to use this technique in lookup functions to data sources. Let’s imagine for a moment we want to find someone’s details in a system of some kind somewhere. The function is going to take an integer ID value and return a Person record.

At least, that’s what you’d often find people doing. You might see something like this:

public Person  GetPerson(int id)
{
    // Fill in some code here.  Whatever data
    // store you want to use.  Except mini-disc.
}

But if you think about it, returning a Person object is only one of the possible return states of the function.

What if an ID is entered for a person who doesn’t exist? We could return null, I suppose, but that doesn’t describe what actually happened. What if there were a handled exception that resulted in nothing being returned? The null doesn’t tell us why it was returned.

The other possibility is an Exception being raised. It might well not be the fault of our code, but nevertheless it could happen if network or other issues arise. What would we return in this case?

Rather than returning an unexplained null and forcing other parts of the codebase to handle it, or an alternative return type object with metadata fields containing exceptions, we could create a DU:

public abstract class PersonLookupResult
{
    public int Id { get; set; }
}

public class PersonFound : PersonLookupResult
{
    public Person Person { get; set; }
}

public class PersonNotFound : PersonLookupResult
{

}

public class ErrorWhileSearchingPerson : PersonLookupResult
{
    public Exception Error { get; set; }
}

We can now return a single class from our GetPersonById() function, which tells the code utilizing the class that one of these three states has been returned, and that state has already been determined. The returned object doesn’t need to have logic applied to it to determine whether it worked, and the states are completely descriptive of each case that needs to be handled.

The function would look something like this:

public PersonLookupResult  GetPerson(int id)
{
    try
    {
        var personFromDb = this.Db.Person.Lookup(id);
        return personFromDb == null
            ? new PersonNotFound { Id = id }
            : new PersonFound
                {
                    Person = personFromDb,
                    Id = id
                };
    }
    catch(Exception e)
    {
        return new ErrorWhileSearchingPerson
        {
            Id = id,
            Error = e
        }
    }
}

And consuming it is once again a matter of using a pattern-matching expression to determine what to do:

public string DescribePerson(int id)
{
    var p = this.PersonRepository.GetPerson(id);
    return p switch
    {
        PersonFound pf => "Their name is " + pf.Name,
        PersonNotFound _ => "Person not found",
        ErrorWhileSearchingPerson e => "An error occurred" + e.Error.Message
    };
}

Sending Email

The preceding example is fine when we’re expecting a value back, but what about when there’s no return value? Let’s imagine I’ve written some code to send an email to a customer or to a family member I can’t be bothered to write a message to myself.9

I don’t expect anything back, but I might like to know if an error has occurred, so this time I’m especially concerned with only two states. This is how I’d define my three possible outcomes from sending an email:

public abstract class EmailSendResult
{

}

public class EmailSuccess : EmailSendResult
{

}

public class EmailFailure : EmailSendResult
{
    pubic Exception Error { get; set; }
}

Use of this class in code might look like this:

public EmailSendResult SendEmail(string recipient, string message)
{
 try
    {
        this.AzureEmailUtility.SendEmail(recipient, message);
        return new EmailSuccess();
    }
    catch(Exception e)
    {
        return new EmailFailure
        {
            Error = e
        };
    }
}

Using the function elsewhere in the codebase would look like this:

var result = this.EmailTool.SendEmail(
    "Season's Greetings",
    "Hi, Uncle John. How's it going?");

var messageToWriteToConsole = result switch
{
    EmailFailure ef => "Error occurred sending the email: " + ef.Error.Message,
    EmailSuccess _ => "Email send successful",
    _ => "Unknow Response"
};

this.Console.WriteLine(messageToWriteToConsole);

This, once again, means we can return an error message and failure state from the function, but without anything anywhere depending on properties it doesn’t need.

Console Input

Some time ago I came up with the mad idea to try out my FP skills by converting an old text-based game written in HP Time-Shared BASIC to functional-style C#.

The game, called Oregon Trail, dated all the way back to 1975. Hard as it is to believe, the game is even older than I am! Older even than Star Wars. In fact, it even predates monitors and had to effectively be played on something that looked like a typewriter. In those days, when the code said print, it meant it!

One of the most crucial things the game code had to do was to periodically take input from the user. Most of the time, an integer was required—either to select a command from a list or to enter an amount of goods to purchase. Other times, it was important to receive text and to confirm what the user typed—such as in the hunting mini-game, where the user was required to type BANG as quickly as possible to simulate attempting to accurately hit a target.

I could have simply had a module in the codebase that returned raw user input from the console. This would mean that every place in the entire codebase that required an integer value would have to carry out an empty string check, followed by parsing the string to an int, before getting on with whatever logic was actually required.

A smarter idea is to use a DU to represent the different states the logic of the game recognizes from user input, and keep the necessary int check code in a single place:

public abstract class UserInput
{

}

public class TextInput : UserInput
{
    public string Input { get; set; }
}

public class IntegerInput : UserInput
{
    public int Input { get; set; }
}

public class NoInput : UserInput
{
}

public class ErrorFromConsole : UserInput
{
 public Exception Error { get; set; }
}

I’m not honestly sure what errors are possible from the console, but I don’t think it’s wise to rule them out, especially as they’re beyond the control of the application code.

The idea here is that we’re gradually shifting from the impure area beyond the codebase and into the pure, controlled area within it (see Figure 6-2)—like a multistage airlock.

stages of text input
Figure 6-2. Stages of text input

Speaking of the console being beyond our control: if we want to keep our codebase as functional as possible, it’s best to hide it behind an interface. Then we can inject mocks during testing and push back the nonpure area of our code a little further:

public interface IConsole
{
    UserInput ReadInput(string userPromptMessage);
}

public class ConsoleShim : IConsole
{
    public UserInput ReadInput(string userPromptMessage)
    {
        try
        {
           Console.WriteLine(userPromptMessage);
           var input = Console.ReadLine();
           return new TextInput
           {
               Input = input
           };
        }
        catch(Exception e)
        {
            return new ErrorFromConsole
            {
                Error = e
            };
        }
    }
}

That was the most basic representation possible of an interaction with the user. That’s because that’s an area of the system with side effects, and we want to keep that as small as possible.

After that, we create another layer, but this time there is some logic applied to the text received from the player:

public class UserInteraction
{
    private readonly IConsole _console;
    public UserInteraction(IConsole console)
    {
     this._console = console;
    }

    public UserInput GetInputFromUser(string message)
    {
        var input = this._console.ReadInput(message);
        var returnValue = input switch
        {
            TextInput x when string.IsNullOrWhiteSpace(x.Input) =>
             new NoInput(),
            TextInput x when int.TryParse(x.Input, out var _)=>
             new IntegerInput
             {
                 Input = int.Parse(x.Input)
             },
            TextInput x => new TextInput
            {
                Input = x.Input
            }
        };

        return returnValue;
    }
}

If we want to prompt the user for input and guarantee that they give us an integer, it’s now easy to code:

public int GetPlayerSpendOnOxen()
{
    var input =
        this.UserInteraction.GetInputFromUser(
            "How much do you want to spend on Oxen?")
    var returnValue = input switch
    {
        IntegerInput ii => ii.Input,
        _ => {
            this.UserInteraction.WriteMessage("Try again");
            return GetPlayerSpendOnOxen();
        }
    };

    return returnValue;
}

In this code block, we’re prompting the player for input. Then, we check whether it’s the integer we expected—based on the check already done on it via a DU. If it’s an integer, great. Job’s a good ’un; return that integer.

If not, the player needs to be prompted to try again, and we call this function again, recursively. We could add more detail about capturing and logging any errors received, but I think this demonstrates the principle soundly enough.

Note also that there isn’t a need for a try/catch in this function. That is already handled by the lower-level function.

There are many, many places this code checking for integer are needed in this Oregon Trail conversion. Imagine how much code we’ve saved ourselves by wrapping the integer check into the structure of the return object!

Generic Unions

All the DUs so far are entirely situation specific. Before wrapping up this chapter, I want to discuss a few options for creating entirely generic, reusable versions of the same idea.

First, let me reiterate: we can’t have DUs that we can simply declare easily, on the fly, as the folks in F# can. It’s just not a thing we can do. Sorry. The best we can do is emulate it as closely as possible, with some sort of boilerplate trade-off.

Here are a couple of functional structures we can use. There are, incidentally, more advanced ways to use these coming up in Chapter 7. Stay tuned for that.

Maybe

If our intention with using a DU is to represent that data might not have been found by a function, then the Maybe structure might be the one for us. Implementations look like this:

public abstract class Maybe<T>
{
}

public class Something<T> : Maybe<T>
{
    public Something(T value)
    {
         this.Value = value;
    }

    public T Value { get; init; }
}

public class Nothing<T> : Maybe<T>
{

}

We’re basically using the Maybe abstract as a wrapper around another class, the actual class our function returns; but by wrapping it in this manner, we are signaling to the outside world that there may not necessarily be anything returned.

Here’s how we might use it for a function that returns a single object:

public Maybe<DoctorWho> GetDoctor(int doctorNumber)
{
    try
    {
         using var conn = this._connectionFactory.Make();
        // Dapper query to the db
        var data = conn.QuerySingleOrDefault<Doctor>(
            "SELECT * FROM [dbo].[Doctors] WHERE DocNum = @docNum",
            new { docNum = doctorNumber });
            return data == null
                ? new Nothing<DoctorWho>();
                : new Something<DoctorWho>(data);
    }
    catch(Exception e)
    {
        this.logger.LogError(e, "Error getting doctor " + doctorNumber);
        return new Nothing<DoctorWho>();
    }

}

We’d use that something like this:

// William Hartnell.  He's the best!
var doc = this.DoctorRepository.GetDoctor(1);
var message = doc switch
{
    Something<DoctorWho> s => "Played by " + s.Value.ActorName,
    Nothing<DoctorWho> _ => "Unknown Doctor"
};

This doesn’t handle error situations especially well. A Nothing state at least prevents unhandled exceptions from occurring, and we are logging, but nothing useful has been passed back to the end user.

Result

An alternative to Maybe is Result, which represents the possibility that a function might throw an error instead of returning anything. It might look like this:

public abstract class Result<T>
{
}

public class Success : Result<T>
{
    public Success<T>(T value)
    {
     this.Value = value;
    }

    public T Value { get; init; }
}

public class Failure<T> : Result<T>
{
    public Failure(Exception e)
    {
        this.Error = e;
    }

    public Exception Error { get; init; }
}

Now, the Result version of the GetDoctor() function looks like this:

public Result<DoctorWho> GetDoctor(int doctorNumber)
{
    try
    {
         using var conn = this._connectionFactory.Make();
        // Dapper query to the db
        var data = conn.QuerySingleOrDefault<Doctor>(
            "SELECT * FROM [dbo].[Doctors] WHERE DocNum = @docNum",
            new { docNum = doctorNumber });
            return new Success<DoctorWho>(data);
    }
catch(Exception e)
{
    this.logger.LogError(e, "Error getting doctor " + doctorNumber);
    return new Failure<DoctorWho>(e);
}

}

And we might consider using it like this:

// Sylvester McCoy.  He's the best too!
var doc = this.DoctorRepository.GetDoctor(7);
var message = doc switch
{
    Success<DoctorWho> s when s.Value == null => "Unknown Doctor!",
    Success<DoctorWho> s2 => "Played by " + s2.Value.ActorName,
    Failure<DoctorWho> e => "An error occurred: " e.Error.Message
};

Now this covers the error scenario in one of the possible states of the DU, but the burden of null checking falls to the receiving function.

Maybe Versus Result

Here’s a perfectly valid question at this point: which is better to use, Maybe or Result? Maybe gives a state that informs the user that no data has been found, removing the need for null checks, but effectively silently swallows errors. It’s better than an unhandled exception but could result in unreported bugs. Result handles errors elegantly but puts the burden on the receiving function to check for null.

My personal preference? This might not be strictly within the standard definition of these structures, but I combine them into one. I usually have a three-state Maybe: Something, Nothing, Error. That handles just about anything the codebase can throw at me.

This would be my solution to the problem:

public abstract class Maybe<T>
{
}

public class Something<T> : Maybe<T>
{
    public Something(T value)
    {
        this.Value = value;
    }

    public T Value { get; init; }
}

public class Nothing<T> : Maybe<T>
{

}

public class Error<T> : Maybe<T>
{
    public Error(Exception e)
    {
        this.CapturedError = e;
    }

    public Exception CapturedError { get; init; }
}

And I’d use it like this:

public Maybe<DoctorWho> GetDoctor(int doctorNumber)
{
    try
    {
        using var conn = this._connectionFactory.Make();
        // Dapper query to the db
        var data = conn.QuerySingleOrDefault<Doctor>(
            "SELECT * FROM [dbo].[Doctors] WHERE DocNum = @docNum",
            new { docNum = doctorNumber });
            return data == null
                ? new Nothing<DoctorWho>();
                : new Something<DoctorWho>(data);
    }
    catch(Exception e)
    {
        this.logger.LogError(e, "Error getting doctor " + doctorNumber);
        return new Error<DoctorWho>(e);
    }

}

The receiving function can now handle all three states elegantly with a pattern-matching expression:

// Peter Capaldi.  The other, other best Doctor!
var doc = this.DoctorRepository.GetDoctor(12);
var message = doc switch
{
    Nothing<DoctorWho> _ => "Unknown Doctor!",
    Something<DoctorWho> s => "Played by " + s.Value.ActorName,
    Error<DoctorWho> e => "An error occurred: " e.Error.Message
};

I find this allows me to provide a full set of responses to any given scenario when returning from a function that requires a connection to the cold, dark, hungry-wolf-filled world beyond my program, and easily allows a more informative response to the end user.

Before we finish on this topic, here’s how I’d use that same structure to handle a return type of IEnumerable:

public Maybe<IEnumerable<DoctorWho>> GetAllDoctors()
{
    try
    {
         using var conn = this._connectionFactory.Make();
        // Dapper query to the db
        var data = conn.Query<Doctor>(
            "SELECT * FROM [dbo].[Doctors]");
            return data == null || !data.Any()
                ? new Nothing<IEnumerable<DoctorWho>>();
                : new Something<IEnumerable<DoctorWho>>(data);
    }
    catch(Exception e)
    {
        this.logger.LogError(e, "Error getting doctor " + doctorNumber);
        return new Error<IEnumerable<DoctorWho>>(e);
    }

}

This allows me to handle the response from the function like this:

// Great chaps.  All of them!
var doc = this.DoctorRepository.GetAllDoctors();
var message = doc switch
{
    Nothing<IEnumerable<DoctorWho>> _ => "No Doctors found!",
    Something<IEnumerable<DoctorWho>> s => "The Doctors were played by: " +
        string.Join(Environment.NewLine, s.Value.Select(x => x.ActorName),
    Error<IEnumerable<DoctorWho>> e => "An error occurred: " e.Error.Message
};

Once again, the code is nice and elegant, and everything has been considered. This is an approach I use all the time in my everyday coding, and I hope after reading this chapter that you will too!

Either

Something and Result—in one form or another—now generically handle returning from a function when there’s some uncertainty as to how it might behave. What if we want to return two or more entirely different types?

This is where the Either type comes in. The syntax isn’t the nicest, but it does work:

public abstract class Either<T1, T2>
{

}

public class Left<T1, T2> : Either<T1, T2>
{
    public Left(T1 value)
    {
        Value = value;
    }

    public T1 Value { get; init; }
}

public class Right<T1, T2> : Either<T1, T2>
{
    public Right(T2 value)
    {
        Value = value;
    }

    public T2 Value { get; init; }
}

We could use it to create a type that might be left or right, like this:

public Either<string, int> QuestionOrAnswer() =>
    new Random().Next(1, 6) >= 4
        ? new Left<string, int>("What do you get if you multiply 6 by 9?")
        : new Right<string, int>(42);

var data = QuestionOrAnswer();
var output = data switch
{
    Left<string, int> l => "The ultimate question was: " + l.Value,
    Right<string, int> r => "The ultimate answer was: " + r.Value.ToString()
};

We could, of course, expand this to have three or more possible types. I’m not entirely sure what we’d call each of them, but it’s certainly possible. A lot of awkward boilerplate is needed, in that we have to include all the references to the generic types in a lot of places. At least it works, though.

Summary

This chapter covered discriminated unions: what exactly they are, how they are used, and just how incredibly powerful they are as a code feature. DUs can be used to massively cut down on boilerplate code, and make use of a data type that descriptively represents all possible states of the system in a way that strongly encourages the receiving function to handle them appropriately.

DUs can’t be implemented quite as easily as in F# or other functional languages, but C# at least offers possibilities.

In the next chapter, I’ll present more advanced functional concepts that will take DUs up to the next level!

1 Let me please reassure everyone that despite being called discriminated unions, they bear no connection to anyone’s view of love and/or marriage or to worker’s organizations.

2 Didn’t I tell you? We’re in the travel business now, you and I! Together, we’ll flog cheap holidays to unsuspecting punters until we retire rich and contented. That, or carry on doing what we’re doing now. Either way.

3 It’s not London Bridge, that famous one you’re thinking of. London Bridge is elsewhere. In Arizona, in fact. No, really. Look it up.

4 No one has ever done this. I’m not aware of a single cat ever being sacrificed in the name of quantum mechanics.

5 Somehow. I’ve never really understood this part of it.

6 Wow. What a horrible birthday present that would be. Thanks, Schrödinger!

7 “先生.” It literally means “one who was born earlier.” Interestingly, if you were to write the same letters in Japanese, it would be pronounced “Sensei.” I’m a nerd—I love stuff like this!

8 Sadly, this is the closest I’ll ever get to him for real. Do watch some of his Hong-Kong films if you haven’t already! I’d start with the Police Story series.

9 Just kidding, folks, honest! Please don’t take me off your Christmas card lists!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.170.81.33