Option Types vs Nullable Types

Some of the feedback that we’ve received about Elevate has to do with Option types and how they are different or similar to Nullable types in C#. Luke Hoban does a great job of describing some of the differences here:

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=470052

If you’ve played around with Option types in F# or another functional language, you should be able to easily understand his argument, but if you haven’t been exposed to Option types before, you’re probably a bit confused as to how they differ from Nullable types. I’ll do my best paraphrasing and cross language explanation here to help you bridge that gap.

In Theory…

In theory, both Option types and Nullable types can be used to accomplish similar goals. Both model calculations that may or may not return a value. For Options and Nullables, an instance either represents a concrete value, or the lack of a value. Let’s take a look at a some examples.

Nullable types in C#:

[Test]
public void Nullables()
{
    int? nullableWithValue = 10;
    Assert.IsTrue(nullableWithValue.HasValue);
    Assert.AreEqual(10, nullableWithValue.Value);

    int? nullableWithoutValue = null;
    Assert.IsFalse(nullableWithoutValue.HasValue);
}

Option Types in F#:

let optionWithValue = Some 10

Assert.IsTrue(optionWithValue.IsSome)
Assert.AreEqual(10, optionWithValue.Value)

let optionWithoutValue = None

Assert.IsFalse(optionWithValue.IsSome)

Note that in F#, it’s common to combine Option types with a technique known as pattern matching which is beyond the scope of this post. Most F# programmers probably wouldn’t use the fields that I demonstrated in the sample code, but they are provided in case you do want to use them.

Option Types with Elevate:

[Test]
public void OptionTypes()
{
    Option<int> optionWithValue = Option.Some(10);
    Assert.IsTrue(optionWithValue.IsSome);
    Assert.AreEqual(10, optionWithValue.Value);
 
    Option<int> optionWithoutValue = Option<int>.None;
    Assert.IsFalse(optionWithoutValue.IsSome);
}

In Practice

In practice, things don’t quite work out the way you’d like. For example, consider a “TryFind” function. This function, given a sequence of elements and a predicate, returns the first element where the predicate returns true, or “no element” if the predicate is not matched. In F#, this is written as “Seq.tryFind”. Let’s take a look at an example usage.

let numbers = [0..10]
let five = numbers
           |> Seq.tryFind ((=) 5) 

Despite the F# syntax, this should be easy for most programmers to understand, but let’s see what this looks like in C# using the (just added) TryFind function in Elevate.

[Test]
public void TryFind()
{
    var values = 0.Through(10);
 
    Option<int> result = values.TryFind(x => x == 5);
 
    Assert.IsTrue(result.IsSome);
    Assert.AreEqual(5, result.Value);
}

Those of you familiar with LINQ will recognize that this looks very similar to the overload of .First that accepts a predicate. The difference is in the case where the predicate does not match any element in the sequence. Instead of throwing an exception, TryFind will return None.

[Test]
public void TryFindOnFailure()
{
    var values = 0.Through(10);
 
    Option<int> result = values.TryFind(x => x == 20);
 
    Assert.IsTrue(result.IsNone);
}

Now that we’ve gone over the usage of TryFind, let’s focus on how we might implement it. Here’s how we currently do it in Elevate (minus a few exception checks).

public static Option<TSource> TryFind<TSource>(this IEnumerable<TSource> source,
                                               Func<TSource, bool> predicate)
{
    var results = source.Where(predicate).GetEnumerator();

    if (results.MoveNext())
    {
        return Option.Some(results.Current);
    }
    else
    {
        return Option<TSource>.None;
    }
}

Now, let’s say that we want to implement TryFind using Nullable types instead of an Option type. You’ll notice right away that there’s a problem. Nullable types only work for structs. TryFind needs to be able to return values of any type, not just value types, so right away, we’re stuck.

There’s one other, slightly more insidious problem, though. Say that we were able to create Nullable types for classes. Our implementation for TryFind would look similar to what we have above for Option types. We would return null when no item in the input sequence was matched, otherwise we would return a value, but consider the following example.

IEnumerable<string> items = Seq.Build("Alpha", "Beta", "Gamma", null);

string result = items.TryFind(item => item == "Delta" || item == null);

Here, our result value would be null, but we wouldn’t know if null meant that no item was found, or that null was the string value that we matched. It’s a subtle and somewhat contrived example, but it does show one more way in which Option types help to clean up the code.

The Bottom Line

To sum things up, Option types and Nullable types are similar in theory, but in practice, they accomplish different goals. In general, I find that using Option types makes for cleaner code and helps to communicate the intent of algorithms more clearly. In Elevate, we use Option types in a few places where Nullable types would not be reasonable. Although these aren’t use cases that you may touch on everyday, it’s definitely good to have the option (no pun intended) to use whatever method makes the most sense for your situation, and that’s why we offer Option types in Elevate.

This entry was posted in C#, Elevate, F#, Functional. Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

4 Comments

  1. James Miles
    Posted September 11, 2009 at 1:03 am | Permalink

    Thanks for that… makes sense.

  2. Robert
    Posted September 15, 2009 at 8:43 am | Permalink

    “Those of you familiar with LINQ will recognize that this looks very similar to the overload of .First that accepts a predicate. The difference is in the case where the predicate does not match any element in the sequence. Instead of throwing an exception, TryFind will return None.”

    Why not using .FirstOrDefault that returns the first element that accepts the predicate or null if there are no such elements?

    Thanks!

  3. cmarinos
    Posted September 15, 2009 at 3:22 pm | Permalink

    @Robert-

    FirstOrDefault might work for you in many of the instances where TryFind is helpful. Remember, though, that null is always a valid value for reference types. If you want to check for null in your predicate, you are in trouble because you don’t know if you matched null or if you didn’t match at all.

    Option types help to clear that up, and I find that once you start to use them, they also significantly improve readability.

    I hope that helps!

    -Chris

  4. AC
    Posted September 16, 2009 at 12:31 am | Permalink

    Good work, and a good explanation.

    It’s often a lot easier to work with Nullable objects, than it is to work with the null reference and null checks. It’s all about writing straightforward, concise code.

    A distinction I also find useful when talking about nullable objects is to ask a person if they were to call a method that returns a list, would they rather have null when there are no results, or a non-null object with an empty list?

    foreach( var item in list.Take(1) )
    // do something for first item
    foreach( var item in list.Skip(1).Take(2) )
    // do something for 2nd and 3rd items

    I know skip and take won’t return null enumerators, so there’s no need for bug-prone index math and state variables to keep track when I want to slice the list and apply different code on different parts of the list.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>