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.