LINQ has always been given as the example of functional programming in C#. It was introduced when C# was mostly an imperative language. This has radically changed in the latest versions of C# but LINQ has stayed unchanged...
I'm no expert in functional programming but it seems to me that methods like First()
, Single()
, Last()
, Min()
, Max()
and Average()
have some issues. They all throw exceptions when the source is empty. Single()
also throws when the source has more than one item. ElementAt()
throws an exception when the index
parameter is out of bounds.
Throwing exceptions causes side effects...
There are some alternative like FirstOrDefault()
, SingleOrDefault()
, LastOrDefault()
, DefaultIfEmpty()
and ElementAtOrDefault()
that return the default
value of the return type when the source is empty or the index
parameter is out of bounds. DefaultIfEmpty()
has a second overload that allows the return of a predefined value. This is still an issue for value-types because the default
value or even the predefined one may be possible values of the source. SingleOrDefault()
still throws when the source has more than one item.
This project is not constrained by breaking changes avoidance. I want it to be a "better" version of LINQ. Keeping a familiar API but taking advantage of newer language features to improve performance and usability.
There are several possible alternatives...
Try pattern
This pattern can be found in many methods. It consist on a method that returns bool
and has an output parameter. The parameter outputs a valid value if the method return true
.
Here's a possible implementation of First()
using this pattern:
public static bool TryFirst<T>(this IEnumerable<T> source, out T value)
{
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
value = enumerator.Current;
return true;
}
value = default;
return false;
}
It can be used this way:
if (source.Where(item => item > 2).TryFirst(out var value))
{
Console.WriteLine(value);
}
One limitation is that output parameters cannot output value-types by reference. Hyperlinq currently returns read-only references in the cases where the source is an array, Span<T>
or equivalent.
Nullable<T>
The method can return a Nullable<T>
:
public static T? First<T>(this IEnumerable<T> source) where T : struct
{
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return enumerator.Current;
return default;
}
The type T
has to be constrained to value-types. We would need an overload for reference-types but, because generic constraints are not part of the method signature, the methods must have different names. The alternative is to add a second generic parameter but it would be very confusing.
This alternative has the same issue as FirstOrDefault()
and SingleOrDefault()
. null
may be a possible value for reference-types, so it would be not possible to differentiate between an empty collections and one that contains one or more null
values.
ValueTuple<bool, T>
The method can return a tuple:
public static (bool HasValue, T Value) First<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return (true, enumerator.Current);
return (false, default);
}
But, it's not good idea to use tuples in public APIs.
Option<T>
The method can return Option<T>
:
public static Option<T> First<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return Option.Some(enumerator.Current);
return Option.None;
}
Possible uses:
// returns first; otherwise throws
var first0 = array
.Where(item => item > 2)
.First()
.Match(
item => item,
() => throw new InvalidOperationException("Sequence contains no elements"));
// returns first; otherwise returns -1
var first1 = array
.Where(item => item > 2)
.First()
.Match(
item => item,
() => -1);
// writes first; otherwise writes <empty>
array
.Where(item => item > 2)
.First()
.Match(
item => Console.WriteLine(item),
() => Console.WriteLine("<empty>"));
It's also possible to add a Desconstruct()
method to Option<T>
:
public void Deconstruct(out bool hasValue, out T value)
{
hasValue = _hasValue;
value = _value;
}
It can then be used the following way:
var (hasValue, value) = array.Where(item => item == 4).First();
if (hasValue)
Console.WriteLine(value);
The C# language does not support fields to be a reference so it would not be possible to return a readonly reference from arrays or Span<T>
. This issue may be worked-around by using ByReference<T>
.
Result<TOk, TError>
What about Single()
? It throws an exception in two different cases. It's the same exception type but with different messages.
It can either return Option<T>
but then there is no way to differentiate the errors.
It can return Result<T, string>
:
public static Result<T, string> Single<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (!enumerator.MoveNext())
return Result.Error("Sequence contains no elements");
var value = enumerator.Current;
if (enumerator.MoveNext())
return Result.Error("Sequence contains more than one element");
return Result.Ok(value);
}
But then, just like the exception, we can only differentiate by comparing strings.
It's possible to differentiate the errors with an enum
type:
public enum SingleError
{
Empty,
Multiple
}
public static Result<T, SingleError> Single<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (!enumerator.MoveNext())
return Result.Error(SingleError.Empty);
var value = enumerator.Current;
if (enumerator.MoveNext())
return Result.Error(SingleError.Multiple);
return Result.Ok(value);
}
Match pattern
An alternative is to use the match pattern without having to call one more method:
public static TOut First<T, TOut>(this IEnumerable<T> source, Func<T, TOut> some, Func<TOut> none)
{
if (some is null) throw new ArgumentNullException(nameof(some));
if (none is null) throw new ArgumentNullException(nameof(none));
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return some(enumerator.Current);
return none();
}
public static void First<T>(this IEnumerable<T> source, Action<T> some, Action none)
{
if (some is null) throw new ArgumentNullException(nameof(some));
if (none is null) throw new ArgumentNullException(nameof(none));
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
some(enumerator.Current);
else
none();
}
I started this journey to avoid throwing exceptions but lets see if it's possible to have the current behavior of First()
and at the same time the match pattern.
public static T First<T>(this IEnumerable<T> source, Func<T> none = null)
{
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return enumerator.Current;
if (none is null)
throw new InvalidOperationException("Sequence contains no elements");
return none();
}
public static TOut First<T, TOut>(this IEnumerable<T> source, Func<T, TOut> some, Func<TOut> none = null)
{
if (some is null) throw new ArgumentNullException(nameof(some));
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
return some(enumerator.Current);
if (none is null)
throw new InvalidOperationException("Sequence contains no elements");
return none();
}
public static void First<T>(this IEnumerable<T> source, Action<T> some, Action none = null)
{
if (some is null) throw new ArgumentNullException(nameof(some));
using var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
some(enumerator.Current);
}
else
{
if (none is null)
throw new InvalidOperationException("Sequence contains no elements");
none();
}
}
Possible uses:
// returns first; otherwise throws
var first0 = array
.Where(item => item > 2)
.First();
// returns first; otherwise returns -1
var first1 = array
.Where(item => item > 2)
.First(() => -1);
// writes first value; otherwise writes <empty>
array
.Where(item => item > 2)
.First(
item => Console.WriteLine(item),
() => Console.WriteLine("<empty>"));
I ended up going full circle. This last solution is compatible with LINQ but, at the same time, supports more functional patterns.
But, can it support the return of references in the case of Span<T>
?
public static ref readonly T First<T>(this ReadOnlySpan<T> source)
{
if (source.Length == 0)
throw new InvalidOperationException("Sequence contains no elements");
return ref source[0];
}
public static T First<T>(this ReadOnlySpan<T> source, Func<T> none)
{
if (none is null) throw new ArgumentNullException(nameof(none));
if (source.Length == 0)
return none();
return source[0];
}
public static TOut First<T, TOut>(this ReadOnlySpan<T> source, Func<T, TOut> some, Func<TOut> none = null)
{
if (some is null) throw new ArgumentNullException(nameof(some));
if (source.Length == 0)
{
if (none is null)
throw new InvalidOperationException("Sequence contains no elements");
return none();
}
return some(source[0]);
}
public static void First<T>(this ReadOnlySpan<T> source, Action<T> some, Action none = null)
{
if (some is null) throw new ArgumentNullException(nameof(some));
if (source.Length == 0)
{
if (none is null)
throw new InvalidOperationException("Sequence contains no elements");
none();
}
else
{
some(source[0]);
}
}
It requires one more overload and only this one returns a reference. Lambdas still do not support passing by reference...