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)
A marble diagram showing the WithIndex operation

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)
A marble diagram showing the WithFirst operation

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)
A marble diagram showing the WithLast operation

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)
A marble diagram showing the WithPrevious operation

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)
A marble diagram showing the InclusiveScan and ExclusiveScan operations

The two variants differ in whether an element's own contribution is included in the state yielded for it:

  • InclusiveScan yields the state after folding in each element. The last value is what Aggregate would return.
  • ExclusiveScan yields 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);