Context for each element
A foreach loop often needs to know more than the element itself: is this the first one, is it the last one,
what came before it, what is its index. The usual answer is a counter or a flag declared outside the loop and
updated inside it. These operations pair each element with that information instead, so it is available in a
Select or a query, and the loop state disappears.
The four With… operations return small structs with a Value property and the extra information, each with a
Deconstruct so they work in tuple patterns and foreach. All are lazy, and when the source is an IList<T>
the result is a list too, so Count and indexing stay cheap.
InclusiveScan and ExclusiveScan generalise the idea: the context is an arbitrary state accumulated over all
preceding elements.
WithIndex
Pairs each element with its zero-based position.
IEnumerable<ValueWithIndex<TSource>> WithIndex<TSource>(this IEnumerable<TSource> source)
ValueWithIndex<T> has a Value and an Index property and deconstructs in that order. Like all the With…
operations it is lazy, and when the source is an IList<T> the result is a list as well, so it keeps Count
and indexing without enumerating.
.NET 9 added Index() to LINQ, which returns (int Index, T Item) tuples. WithIndex is available on every
target framework and puts the value first, matching the other With… types.
Example
var names = Sequence.Return("Ada", "Grace", "Linus");
names.WithIndex().Select(n => $"{n.Index + 1}. {n.Value}");
// ["1. Ada", "2. Grace", "3. Linus"]
foreach (var (name, index) in names.WithIndex())
{
Console.WriteLine($"{index}: {name}");
}
WithFirst
Pairs each element with a flag telling whether it is the first one.
IEnumerable<ValueWithFirst<TSource>> WithFirst<TSource>(this IEnumerable<TSource> source)
ValueWithFirst<T> has Value and IsFirst and deconstructs in that order. Exactly one element has
IsFirst == true, unless the sequence is empty. The operation is lazy and streams; it needs no lookahead.
Example
Rendering a list where the first entry gets special treatment:
var lines = steps.WithFirst().Select(step => step.IsFirst
? $"Start with {step.Value}"
: $"then {step.Value}");
This is also how Intersperse is implemented: emit a separator before every element that is not the first.
WithLast
Pairs each element with a flag telling whether it is the last one.
IEnumerable<ValueWithLast<TSource>> WithLast<TSource>(this IEnumerable<TSource> source)
ValueWithLast<T> has Value and IsLast and deconstructs in that order. To know that an element is the last
one, WithLast has to try reading the next one first. On a plain IEnumerable<T> it therefore runs one element
behind the source: each element is yielded only when its successor has been read, or the end has been reached.
On an IList<T> the count is known and there is no delay.
The delay matters for sources that block, such as a stream of events: the last event is not visible until the source completes.
Example
Joining with a terminator, in the way string.Join cannot:
var sql = columns
.WithLast()
.Select(column => column.IsLast ? $"{column.Value};" : $"{column.Value},")
.ConcatToString();
WithPrevious
Pairs each element with the element before it, as an Option.
IEnumerable<ValueWithPrevious<TSource>> WithPrevious<TSource>(this IEnumerable<TSource> source)
ValueWithPrevious<T> has Value and Previous, where Previous is None for the first element and Some
of the predecessor for every other one. The result has as many elements as the input. Compare
Pairwise, which produces tuples of neighbours and therefore one
element fewer, dropping the first.
The element type is constrained to notnull, because Previous is an Option<T>.
Example
Marking where a sorted sequence changes value:
var readings = Sequence.Return(1, 1, 2, 2, 2, 3);
readings
.WithPrevious()
.Where(r => r.Previous.Select(p => p != r.Value).GetOrElse(true))
.Select(r => r.Value);
// [1, 2, 3]
InclusiveScan, ExclusiveScan
A scan is an Aggregate that yields every intermediate state instead of only the final one. The classic
example is the running total, also called the prefix sum.
IEnumerable<TAccumulate> InclusiveScan<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> accumulator)
IEnumerable<TAccumulate> ExclusiveScan<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> accumulator)
The two variants differ in whether an element's own contribution is included in the state yielded for it:
InclusiveScanyields the state after folding in each element. The last value is whatAggregatewould return.ExclusiveScanyields the state before folding in each element. The first value is the seed, and the final accumulated state is never yielded.
Both produce exactly as many elements as the input and are lazy, which makes them usable on infinite sequences.
Example
var amounts = Sequence.Return(10, 20, 30);
amounts.InclusiveScan(0, (sum, amount) => sum + amount);
// [10, 30, 60]
amounts.ExclusiveScan(0, (sum, amount) => sum + amount);
// [0, 10, 30]
The exclusive scan answers "where does each element start", for example the offset of each block in a buffer:
var offsets = blocks.ExclusiveScan(0, (offset, block) => offset + block.Length);
The inclusive scan answers "what is the state so far", for example an account balance after every transaction:
var balances = transactions.InclusiveScan(openingBalance, (balance, t) => balance + t.Amount);