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.
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.
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.
This also means we can have a single array of all CustomerOffering
s, 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.
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.
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.
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
};
}
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.
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.
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!
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.
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.
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.
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
;
}
}
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!
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.
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!
35.170.81.33