loading...

Let's Implement an Option Type in C#

ntreu14 profile image Nicholas Treu ・9 min read

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 or Monad. 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 Funcs. 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!

Posted on by:

ntreu14 profile

Nicholas Treu

@ntreu14

Software Engineer with a love for functional programming.

Discussion

pic
Editor guide