Combining IEnumerable with Option/Maybe in LINQ queries
October 13, 2014 Leave a comment
I have found that Option<T>/Maybe<T> (same as Nullable, but can be used with reference types and LINQ) to be incredibly useful, especially with LINQ support. Usefulness of Option/Maybe is well documented, so is also the LINQ support for Maybe<T> or Option<T>. What I want to briefly address here is the interplay between Option<T> and IEnumerable<T>. Essentially about flattening IEnumerable<Option<T>> to just IEnumerable<T> and Option<IEnumerable<T>> to IEnumerable<T>:
IEnumerable<Option<T>> => IEnumerable<T>
Option<IEnumerable<T>> => IEnumerable<T>
In the first case I want to avoid having to deal with an enumeration of holes, which in general is meaningless, while in the second an empty Option<IEnumerable<T>> can just be expressed as an empty IEnumerable<T>.
Taking concrete examples, this is what I would like to be able to write. Let’s first define a few helper methods to make our examples a little bit more realistic:
public static Option<V> GetValue<K, V>(this Dictionary<K, V> that, K key) {
V value;
return that.TryGetValue(key, out value)
? new Option<V>(value)
: Option.Empty<V>();
}
public static Option<double> div(int n, int d) {
return d == 0
? Option.Empty<double>()
: new Option<double>(1.0n/d);
}
public static Option<double> inv(int n) {
return n == 0
? Option.Empty<double>()
: new Option<double>(1.0/n);
}
The simplest case consists of only selecting Options that have a value in a standard IEnumerable query:
IEnumerable<double> invs = from i in new[] { 0, 1, 2 }
select inv(i);
which contains just two elements. The same can be done with an Option sub query:
IEnumerable<double> invs = from i in new[] { 0, 1, 2 }
select from v in inv(i)
select v;
A more complex example:
IEnumerable<double> res = from i in new[] {1, 2}
from j in new[] {0, 2}
select div(i, j);
For the other operation we use a dictionary of IEnumerable to illustrate the implementation:
var d = new Dictionary<int, IEnumerable<int>>() {
{1, new[] {1, 2, 3}}
};
IEnumerable<int> res = from v in d.GetValue(1)
from w in d.GetValue(1)
select v.Concat(w);
The following example returns an empty enumeration:
IEnumerable<int> res = from v in d.GetValue(1)
from w in d.GetValue(0)
select v.Concat(w);
Queries can also be nested:
IEnumerable<int> res = from e in from v in d.GetValue(1)
select v
select e*2;
Here are the overloads I defined to support these examples:
public static IEnumerable<R> Select<T,R>(this IEnumerable<T> source, Func<T, Option<R>> selector) {
foreach (var s in source) {
var r= selector(s);
if (r.HasValue) yield return r.Value;
}
}
public static IEnumerable<R> SelectMany<T,C,R>(this IEnumerable<T> source, Func<T, IEnumerable<C>> collectionSelector, Func<T, C, Option<R>> resultSelector) {
foreach (var s in source) {
foreach (var r in collectionSelector(s)) {
var result= resultSelector(s, r);
if (result.HasValue) yield return result.Value;
}
}
}
public static IEnumerable<R> SelectMany<T,O,R>(this Option<T> that, Func<T, Option<O>> optionSelector, Func<T, O, IEnumerable<R>> selector) {
if (that.HasValue) {
var result = optionSelector(that.Value);
if (result.HasValue) return selector(that.Value, result.Value);
}
return Enumerable.Empty<R>();
}
public static IEnumerable<R> Select<T,R>(this Option<T> that, Func<T, IEnumerable<R>> selector) {
return that.HasValue
? selector(that.Value)
: Enumerable.Empty<R>();
}
For the record here are the LINQ extension methods for Option:
public static Option<R> SelectMany<T, O, R>(this Option<T> that, Func<T, Option<O>> optionSelector, Func<T, O, R> selector) {
if (that.HasValue) {
var option = optionSelector(that.Value);
if (option.HasValue) {
return new Option<R>(selector(that.Value, option.Value));
}
}
return Option.Empty<R>();
}
public static Option<R> SelectMany<T, O, R>(this Option<T> that, Func<T, Option<O>> optionSelector, Func<T, O, Option<R>> selector) {
if (that.HasValue) {
var option = optionSelector(that.Value);
if (option.HasValue) return selector(that.Value, option.Value);
}
return Option.Empty<R>();
}
public static Option<R> SelectMany<T, R>(this Option<T> that, Func<T, Option<R>> selector) {
if (that.HasValue) {
var selected = selector(that.Value);
if (selected.HasValue) return selected;
}
return Option.Empty<R>();
}
public static Option<R> Select<T, R>(this Option<T> that, Func<T, R> selector) {
return that.HasValue
? new Option<R>(selector(that.Value))
: Option.Empty<R>();
}
There are be other ways of combining Option and IEnumerable, but quite often I ended up confusing LINQ as it was unable to make its choice between two or more overloads. So far, I have only used the first two methods in production code, and they have nicely demonstrated their worth in terms of readability and conciseness.