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.