Skip to content

Option User Guide

Martin Marciniszyn Mehringer edited this page Apr 4, 2019 · 19 revisions

User Guide for Option<T> and Maybe<T>

This guide describes the programming interface of Option<T> and Maybe<T>. An executable version of the code presented below can be found in OptionDemo.

Comparing Option<T> to T?

Option<T> and Maybe<T> provide essentially the same programming interface as Nullable<T> (T? for short), but allow T to be either any type or a class type, respectively.

Initialization

Just like T?, Option<T> can be initialized with null, default, or any valid value of type T:

double? nullDouble = null;
Option<string> nullString = null;
double? someDouble = 3.14;
Option<string> someString = "foo";

Basic Methods

We introduce a Print helper function so as to keep the code examples concise:

static void Print(object value = default) => Console.WriteLine(value);

We can check the presence of a valid value with HasValue:

Print(nullDouble.HasValue); // False
Print(nullString.HasValue); // False
Print(someDouble.HasValue); // True
Print(someString.HasValue); // True

We employ another helper function for dealing with exceptions:

static object TryCatch<T>(Func<T> func)
{
    try { return func(); } catch (Exception ex) { return $"{ex.GetType()}: {ex.Message}"; }
}

Property Value provides direct access to the value stored in Option<T>:

Print(someDouble.Value);                 // 3.14
Print(someString.Value);                 // foo
Print(TryCatch(() => nullDouble.Value)); // System.InvalidOperationException: Nullable object must have a value.
Print(TryCatch(() => nullString.Value)); // System.InvalidOperationException: Nullable object must have a value.

One can also access the value with an explicit cast:

Print((double)someDouble);                 // 3.14
Print((string)someString);                 // foo
Print(TryCatch(() => (double)nullDouble)); // System.InvalidOperationException: Nullable object must have a value.
Print(TryCatch(() => (string)nullString)); // System.InvalidOperationException: Nullable object must have a value.

Access the value or return default(T) using method GetValueOrDefault():

Print(nullDouble.GetValueOrDefault()); // 0
Print(nullString.GetValueOrDefault()); //
Print(someDouble.GetValueOrDefault()); // 3.14
Print(someString.GetValueOrDefault()); // foo

Or provide a custom default value:

Print(nullDouble.GetValueOrDefault(42));    // 42
Print(nullString.GetValueOrDefault("bar")); // bar
Print(someDouble.GetValueOrDefault(42));    // 3.14
Print(someString.GetValueOrDefault("bar")); // foo

Null Coalescing Operator

The null coalescing operator (??) is not directly supported on Option<T>. Instead, we call the method GetValueOr(Func<T>). That is, we provide a function that can supply a default value similar to this one:

static T Computation<T>(T value)
{
    Print("running computation ...");
    return value;
}

We can use it in GetValueOr(Func<T>) like so:

Print(nullDouble ?? Computation(42.0));                 // running computation ... 42
Print(nullString.GetValueOr(() => Computation("bar"))); // running computation ... bar
Print(someDouble ?? Computation(42.0));                 // 3.14
Print(someString.GetValueOr(() => Computation("bar"))); // foo

We can use the same construct to throw custom exceptions when there is no valid value:

Print(TryCatch(() => nullDouble ?? throw new Exception("missing double")));                // System.Exception: missing double
Print(TryCatch(() => nullString.GetValueOr(() => throw new Exception("missing string")))); // System.Exception: missing string
Print(TryCatch(() => someDouble ?? throw new Exception("missing double")));                // 3.14
Print(TryCatch(() => someString.GetValueOr(() => throw new Exception("missing string"))))  // foo

Null Conditional Operator and Lifted Conversion Operators

The null conditional operator (?.) is emulated by method Select<TOut>(Func<T, TOut>):

Print(nullDouble?.CompareTo(0));                   // None<Int32>
Print(nullString.Select(x => x.CompareTo("bar"))); // None<Int32>
Print(someDouble?.CompareTo(0));                   // Some<Int32>(1)
Print(someString.Select(x => x.CompareTo("bar"))); // Some<Int32>(1)

Here we call custom overloads of the Print function for the sake of clarity:

static void Print<T>(Option<T> opt)
{
    var typename = typeof(T).Name;
    Print(opt.HasValue ? $"Some<{typename}>({opt})" : $"None<{typename}>");
}

static void Print<T>(T? opt) where T : struct
{
    var typename = typeof(T).Name;
    Print(opt.HasValue ? $"Some<{typename}>({opt})" : $"None<{typename}>");
}

Similarly, we can emulate lifted conversion operators:

Print(2 * 6371 * nullDouble);             // None<Double>
Print(nullString.Select(x => x + "bar")); // None<String>
Print(2 * 6371 * someDouble);             // Some<Double>(40009.88)
Print(someString.Select(x => x + "bar")); // Some<String>(foobar)

LINQ Type Operations

Any option type can be regarded as a set containing either zero or one element. Hence, we should be able to apply LINQ type operations to it like Where, Contains, Any, All, SelectMany, etc. This is also true for Nullable<T>, for which we provide corresponding extension methods in Nullables. These allow us to write code of the following sort:

Print(nullDouble.Where(x => x >= 0).Select(Math.Sqrt));       // None<Double>
Print(nullString.Where(x => x.Length > 0).Select(x => x[0])); // None<Char>
Print(someDouble.Where(x => x >= 0).Select(Math.Sqrt));       // Some<Double>(1.77200451466693)
Print(someString.Where(x => x.Length > 0).Select(x => x[0])); // Some<Char>(f)

Or equivalently in LINQ syntax:

Print(from x in nullDouble where x >= 0 select Math.Sqrt(x)); // None<Double>
Print(from x in nullString where x.Length > 0 select x[0]);   // None<Char>
Print(from x in someDouble where x >= 0 select Math.Sqrt(x)); // Some<Double>(1.77200451466693)
Print(from x in someString where x.Length > 0 select x[0]);   // Some<Char>(f)

Match Expressions

The paradigmatic way of handling Option types in functional programming languages is through pattern matching. Along these lines, the library provides a Match method for Option<T> and Nullable<T>:

Print(nullDouble.Match(some: x => x + x, none: () => Computation(42)));    // running computation ... 42
Print(nullString.Match(some: x => x + x, none: () => Computation("bar"))); // running computation ... bar
Print(someDouble.Match(some: x => x + x, none: () => Computation(42)));    // 6.28
Print(someString.Match(some: x => x + x, none: () => Computation("bar"))); // foofoo

Similarly, there exists a ForEach method to consume the value within the option type.

Reducing the Memory Footprint with Maybe<T>

Programming Examples

Clone this wiki locally