Note: The purpose of this post is to discuss a basic explanation, implementation and use of an Option type. It isn't to dive into more advanced functional programming topics like
Functor
,Applicative
orMonad
. I'm also not suggesting using options everywhere to model your domain. There are much better ways to model your domain using functional concepts. I highly recommend Scott Wlaschin's Domain Driven Design talk if you would like to learn more about the topic.
Many functional programming languages have a type called Option
(F#, Scala, OCAML) or Maybe
(Haskell, Elm, PureScript) as part of their core library. It allows the programmer to encode in the language's type system that data may or may not be present at run time. The compiler then becomes your friend and goes to work for you. It forces (or at least warns) the programmer to handle the case of when there is no data to operate on.
Let's jump into a quintessential example for the use of an Option type. Consider a function that does simple integer division. In C#, we might write something like:
public static int IntDivide(int dividend, int divisor) =>
dividend / divisor;
But what if the divisor
is 0
? If we ran this method with 0
as the divisor
we would get a DivideByZeroException
.
Sure, we could throw an ArgumentException
like so:
public static int IntDivide(int dividend, int divisor)
{
if (divisor == 0)
throw new ArgumentException("Cannot divide by 0.");
return dividend / divisor;
}
A little better, but we still have the same problem - catching exceptions. It quickly becomes cumbersome to catch an exception every time we want to use our IntDivide
method. What if we had a way to operate over data that could perhaps be there or not at runtime, without using exceptions to indicate that we went down an invalid code path?
Enter the Option Type
In F#, the Option type is defined like so:
type Option<'a> =
| Some of 'a
| None
If you're not familiar with F# syntax, you can think of the above union type definition like this: Option
is a type that has only two possible cases. Some
that encapsulates "some" data of a generic type 'a
and None
representing no data at all.
If we were to write our IntDivide
function in F# using an option type it would look something like this:
// int -> int -> int option
let intDivide dividend divisor =
if divisor = 0
then None
else Some (dividend / divisor)
If the divisor
is 0
, we are going to return None
indicating that there isn't a result of dividing by 0
or Some
with the value of the integer division "wrapped inside".
Now any time we call the intDivide
function, we as programmers must handle both cases of the int option
or F# will give a warning that not all cases are matched:
let resultString =
match intDivide 4 0 with
| Some quotient -> sprintf "Matched Some case, quotient is: %d" quotient
| None -> "Matched None case because we divided by 0."
printfn "%s" resultString
Can we implement similar behavior in C#? Absolutely!
Implementation of Option in C#
C# doesn't have support for union types, but we can come fairly close. Let's define an interface with method called Match
where we model the F# behavior above:
interface IOption<T>
{
TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> onNone);
}
Now let's implement Some
and None
from our interface.
class Some<T> : IOption<T>
{
private T _data;
private Some(T data)
{
_data = data;
}
public static IOption<T> Of(T data) => new Some<T>(data);
public TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> _) =>
onSome(_data);
}
class None<T> : IOption<T>
{
public TResult Match<TResult>(Func<T, TResult> _, Func<TResult> onNone) =>
onNone();
}
This interface method takes two Func
s. The first Func
is applied to the value that is encapsulated inside of the Some
class. The second wraps a value of the same type as the result of the first Func
, and is used for the None
class.
Now to replicate the behavior we see above in F#, we can modify the C# IntDivide
to look like:
public static IOption<int> IntDivide(int dividend, int divisor) =>
divisor == 0 ? new None<int>() : Some<int>.Of(dividend / divisor);
var resultString =
IntDivide(4, 0)
.Match<string>(
onSome: quotient => $"Matched Some case, quotient is: {quotient}",
onNone: () => "Matched None case because we divided by 0."
);
Console.WriteLine(resultString);
Match
is a nice start, but it leaves a lot to be desired. What if we wanted to take our result and then divide it again using our IntDivide
method? Well, since we are dealing with IOption<int>
here, it would look something like this:
IntDivide(10, 5)
.Match(
onSome: quotient1 =>
IntDivide(quotient1, 2)
.Match(
onSome: quotient2 => quotient2,
onNone: () => ??
),
onNone: () => ??
);
Yuck. Apart from the Triangle of Doom code pattern we see beginning to emerge, we should also notice a couple of other concerns. First, what do we return when we are dividing by 0
? And worse, because of the way we wrote this code, we have to handle it twice. For our purposes well handle the None
cases in a moment, but we are explicitly forced to handle those cases in code instead of the error handling being exception driven. This should be seen as a good thing! We have to make a decision about what to do when dividing by 0
in our code in order to get it to compile.
Secondly, we should notice that we are doing a Match
inside of a Match
. Surely there must be a cleaner way to do this and lucky for us there is! Bind
.
For IOption
, you can think of Bind
as a way to compose computations that also return IOption
. This is exactly what we want with our IntDivide
example, we want to be able to take the result from the first IntDivide
and compose it with another call to IntDivide
.
Let's add Bind
to our IOption
interface and then implement it for None
and Some
:
interface IOption<T>
{
TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> onNone);
IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f);
}
class Some<T> : IOption<T>
{
private T _data;
private Some(T data)
{
_data = data;
}
public static IOption<T> Of(T data) => new Some<T>(data);
public TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> _) =>
onSome(_data);
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => f(_data);
}
class None<T> : IOption<T>
{
public TResult Match<TResult>(Func<T, TResult> _, Func<TResult> onNone) =>
onNone();
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => new None<TResult>();
}
Our ugly code above becomes:
IntDivide(10, 5)
.Bind(quotient => IntDivide(quotient, 2))
.Match(
onSome: Convert.ToDouble
onNone: () => double.PositiveInfinity
);
That looks much better and much more clean. We were able to implement a way to compose computations that also return IOption
without having to do a nested Match
. Bind
takes a Func
with the argument being the encapsulated result of the previous computation and it returns an IOption
, the result of the composed computation.
But we also have another benefit as well. We were able to abstract way handling the None
case until the end! We implemented a way that if any of our computations in our chain return an instance of None
, then any computations afterwards won't be executed at all.
For example, if we change our code above to:
IntDivide(10, 0)
.Bind(quotient => IntDivide(quotient, 2))
.Match(
onSome: Convert.ToDouble
onNone: () => double.PositiveInfinity
);
We see that right way IntDivide
is going to return an instance of None
because we are dividing by 0
. None
's implementation for Bind
is just to return a new instance of None
, so the Func
that we passed to Bind
to do another IntDivide
is ignored. We're able to create chains of computations that has the error handling built into it, and we can choose what to do if any computation along the way yields None
until the end of our chain of computations!
Great! But let's keep adding to our toolbox. Not everything that we want to work with is going to return an IOption
. What about an operation like adding? Adding integers is pretty straightforward, in the majority of use cases there isn't a need to return an IOption
.
public static int IntAdd(int a, int b) => a + b;
It seems kind of silly to do something like this since we aren't ever going to encounter a None
case from the computation passed to Bind
:
IntDivide(10, 5).Bind(quotient => Some<int>.Of(IntAdd(quotient, 3)));
So let's add a method to our interface to handle cases where we want to work with computations that themselves don't return IOption
. We're going to call it Map
and implement it for Some
and None
.
interface IOption<T>
{
TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> onNone);
IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f);
IOption<TResult> Map<TResult>(Func<T, TResult> f);
}
class Some<T> : IOption<T>
{
private T _data;
private Some(T data)
{
_data = data;
}
public static IOption<T> Of(T data) => new Some<T>(data);
public TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> _) =>
onSome(_data);
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => f(_data);
public IOption<TResult> Map<TResult>(Func<T, TResult> f) => new Some<TResult>(f(_data));
}
class None<T> : IOption<T>
{
public TResult Match<TResult>(Func<T, TResult> _, Func<TResult> onNone) =>
onNone();
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => new None<TResult>();
public IOption<TResult> Map<TResult>(Func<T, TResult> f) => new None<TResult>();
}
Now let's use Map
in our example we've been working with:
IntDivide(10, 5)
.Bind(quotient => IntDivide(quotient, 2))
.Map(quotient => IntAdd(quotient, 3))
.Match(
onSome: Convert.ToDouble
onNone: () => double.PositiveInfinity
);
We've created a Method called Map
that takes a Func
where its argument is the result of the computation before it and it's result is a type that isn't IOption
, in this case int
. Looking at the implementations of Some
and None
for Map
, we see we get the same error handling benefits we did with Bind
. If the computation before it is an instance of None
, return a new instance of None
, otherwise apply the Func
to _data
inside of Some
and create a new instance of Some
from it's result.
I've intentionally ignored dicussing what the result should be if we match the None
case at the end until now. For our purposes, it illustrates the point of Match
to return PositiveInfinity
if we divide by 0
, otherwise convert the int
wrapped inside the option to a double
and return the double
.
In the real world, returning PositiveInfinity
for the None
case most likely isn't what is going to happen. But the choice is up to you! If this is an API, maybe you decide you're API isn't going to accept divisors of 0
and you return a Bad Request to indicate that. Or maybe at the end of our chain, you decide that dividing by 0
truly is an exceptional case and you throw an exception. That choice is on the programmer, but what's great here is that you are required to make that choice by the compiler. This is going to lead to less bugs because you're forced to think about what to do when you do get a None
.
Let's implement one more method for IOption
. Often times when working with options, we just want the value "wrapped inside" of the option or some other value if the option is None
. It can become a little tiresome to write out Match
for these situations. Let's implement a method called Or
on our IOption
interface to make these situations easier on ourselves. This is going to give us the value encapsulated inside Some
or the default value you pass to Or
on None
.
interface IOption<T>
{
TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> onNone);
IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f);
IOption<TResult> Map<TResult>(Func<T, TResult> f);
T Or(T aDefault);
}
class Some<T> : IOption<T>
{
private T _data;
private Some(T data)
{
_data = data;
}
public static IOption<T> Of(T data) => new Some<T>(data);
public TResult Match<TResult>(Func<T, TResult> onSome, Func<TResult> _) =>
onSome(_data);
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => f(_data);
public IOption<TResult> Map<TResult>(Func<T, TResult> f) => new Some<TResult>(f(_data));
public T Or(T _) => _data;
}
class None<T> : IOption<T>
{
public TResult Match<TResult>(Func<T, TResult> _, Func<TResult> onNone) =>
onNone();
public IOption<TResult> Bind<TResult>(Func<T, IOption<TResult>> f) => new None<TResult>();
public IOption<TResult> Map<TResult>(Func<T, TResult> f) => new None<TResult>();
public T Or(T aDefault) => aDefault;
}
We can finally write our example in a nice fluent way:
IntDivide(10, 5)
.Bind(quotient => IntDivide(quotient, 2))
.Map(quotient => IntAdd(quotient, 3))
.Map(Convert.ToDouble)
.Or(double.PositiveInfinity);
If you'd like to see more C# option code, including a helper classes that leverages options for operations like trying to find elements in a dictionary, I've created a Gist to explore that code if you'd like.
Thanks for reading!
Top comments (0)