Standard Library
class Seq<T>
A lazy sequence: a pipeline of steps that does no work until its result is requested.
since 0.1.0-alpha.1linuxwindowswasm
- bases
IIterable<T>
Overview
Arrays are eager, meaning each method such as map finishes over the whole array before the
next step starts. A Seq is the lazy alternative. Get one from Array.asSeq(), chain the
combinators map, where, take, takeWhile and skip (each only describes a step and
runs nothing), and finish with a terminal operation: toArray, firstOrNone, count,
forEach or reduce. Only then are elements pulled through, one at a time, and each function
runs at most once per element that is actually pulled. take(n) and firstOrNone stop pulling
early, so they work even when later elements are expensive or never needed.
A terminal operation needs the sequence to be finite; take is the way to bound one that is
not. Running a terminal operation again on the same Seq starts over from the beginning of an
array-backed pipeline.
Description
Arrays are eager: map and where on an Array run to completion and return a new array.
Seq<T> is the lazy alternative. array.asSeq() turns an array into a Seq, and any
IIterable<T> can join a pipeline by wrapping it in IterableSeq.
A Seq has two kinds of methods.
- Combinators return another
Seqand do no work:map,where,take,takeWhileandskip. - Terminals pull elements through the pipeline and return a result:
toArray,firstOrNone,count,forEachandreduce.
Nothing runs until a terminal asks for an element. Then each element is pulled through the whole
pipeline one at a time, so a function given to map or where runs at most once per element
that is actually pulled. firstOrNone pulls only until it has one element, and take(n) stops
pulling after n.
Evaluation order is element by element
Array<int> nums = [1, 2, 3, 4, 5, 6, 7];
Array<int> r = nums.asSeq()
.map((x) => { console.writeln("square ${x}"); return x * x; })
.where((x) => { console.writeln("odd? ${x}"); return x % 2 == 1; })
.take(2)
.toArray();
console.writeln(r);
square 1
odd? 1
square 2
odd? 4
square 3
odd? 9
[1, 9]
The trace shows that each element goes through map and then where before the next element
starts, and that the pipeline stops as soon as take(2) has its two results: the source elements
4 to 7 are never visited.
A Seq does not store its results. Each terminal call starts again from the source, so calling two
terminals on the same Seq runs its functions twice. Call toArray() once if you need to reuse
the elements.
Terminals need a finite source. A source that never ends is made finite with take(n) (or
takeWhile); there is no guard against a terminal on an endless source, the same as a
while (true) loop has none.
Laziness and terminals
Array<int> nums = [1, 2, 3, 4];
Seq<int> lazy = nums.asSeq().map((x) => { console.writeln("map ${x}"); return x * 10; });
console.writeln("built");
int? first = lazy.firstOrNone();
console.writeln(first);
console.writeln(lazy.count());
console.writeln(lazy.take(2).toArray());
console.writeln(nums.asSeq().takeWhile((x) => x < 3).toArray());
console.writeln(nums.asSeq().skip(1).reduce(0, (a, x) => a + x));
built
map 1
10
map 1
map 2
map 3
map 4
4
map 1
map 2
[10, 20]
[1, 2]
9
Rules
- Combinators (
map,where,take,takeWhile,skip) are lazy and pull nothing. - Terminals (
toArray,firstOrNone,count,forEach,reduce) drive the pipeline. - A
maporwherefunction runs at most once per pulled element, and elements are pulled one at a time through the whole pipeline. firstOrNonepulls exactly as many elements as it needs to find one.- A terminal on an unbounded source never returns; bound the source with
take. - Each terminal re-runs the pipeline from the source. Nothing is cached.
- Pipelines are pure over their source: the array or iterable you start from is not changed.
Examples
Taking from an endless iterable
class Naturals : IIterable<int> {
IIterator<int> iterator() => NaturalsIterator();
}
class NaturalsIterator : IIterator<int> {
int cur = 0;
bool hasNext() => true;
int next() { cur = cur + 1; return cur; }
}
Seq<int> naturals = IterableSeq(Naturals());
console.writeln(naturals.where((n) => n % 2 == 0).take(3).toArray());
Array<int> eager = [1, 2, 3].map((x) => { console.writeln("eager ${x}"); return x; });
console.writeln(eager.length());
[2, 4, 6]
eager 1
eager 2
eager 3
3
Notes
Each Seq stage wraps its source and pulls from it through the IIterable/IIterator
interfaces, which is why any type that implements the iterator protocol can feed a pipeline.
Examples
Laziness shown by a trace
Array<int> nums = [1, 2, 3, 4, 5, 6];
Seq<int> pipeline = nums.asSeq()
.where((x) => {
console.writeln("check ${x}");
return x % 2 == 0;
})
.map((x) => x * 10)
.take(2);
console.writeln("built, nothing has run yet");
console.writeln(pipeline.toArray());
built, nothing has run yet
check 1
check 2
check 3
check 4
[20, 40]
Methods
count
count() -> intRun the pipeline and count how many elements it produces.
This is a terminal operation that pulls every element. The sequence must be finite.
Returns
The number of elements produced.
Examples
Counting matches
Seq<int> s = [1, 2, 3, 4, 5, 6].asSeq().where((x) => x % 3 == 0);
console.writeln(s.count());
2
firstOrNone
firstOrNone() -> T | NoneRun the pipeline just far enough to produce its first element.
This is a terminal operation that pulls exactly one element through the steps, so the rest of the source is never examined.
Returns
The first element, or None when the sequence is empty.
Examples
Short-circuiting
Seq<int> s = [5, 6, 7].asSeq().map((x) => {
console.writeln("map ${x}");
return x + 1;
});
console.writeln(s.firstOrNone());
console.writeln([1].asSeq().skip(1).firstOrNone());
map 5
6
None
forEach
forEach((T) => void fn) -> voidRun the pipeline and call fn on every element it produces, in order.
This is a terminal operation that pulls every element. The sequence must be finite.
Parameters
- fn
- The action to perform on each element.
Examples
Printing as elements arrive
[1, 2, 3].asSeq().map((x) => x * 2).forEach((x) => console.writeln("got ${x}"));
got 2
got 4
got 6
iterator
iterator() -> IIterator<T>Return an iterator over the sequence, starting from the beginning.
Every sequence that asSeq and the combinators produce implements this. The base Seq class
is abstract: a class you write that extends Seq must override iterator(), and the base
version throws.
Returns
An iterator that yields the elements of the sequence lazily.
Throws
RuntimeException- when called on a
Seqthat does not override it.
Examples
Pulling elements by hand
Seq<int> s = [10, 20, 30].asSeq().map((x) => x + 1);
IIterator<int> it = s.iterator();
while (it.hasNext()) {
console.writeln(it.next());
}
11
21
31
map
map<U>((T) => U fn) -> Seq<U>Return a lazy sequence of fn applied to each element.
Nothing is computed until a terminal operation pulls elements. The element type of the result
may differ from T.
Parameters
- fn
- The transformation applied to each pulled element.
Returns
A new lazy sequence.
Examples
Map is not run until pulled
Seq<string> s = [1, 2, 3].asSeq().map((x) => {
console.writeln("map ${x}");
return "n" + x.toString();
});
console.writeln("created");
console.writeln(s.firstOrNone());
created
map 1
n1
See also: map
reduce
reduce<A>(A seed, (A, T) => A fn) -> ARun the pipeline and fold its elements into one value, from first to last.
This is a terminal operation that pulls every element. The sequence must be finite. An empty
sequence returns seed.
Parameters
- seed
- The starting value of the accumulator.
- fn
- Combines the accumulator so far with the next element and returns the new accumulator.
Returns
The final accumulator.
Examples
Summing a filtered sequence
int total = [1, 2, 3, 4].asSeq().where((x) => x > 1).reduce(0, (sum, x) => sum + x);
console.writeln(total);
9
skip
skip(int n) -> Seq<T>Return a lazy sequence that leaves out the first n elements.
If the source has n or fewer elements the result is empty. The skipped elements are still
pulled from the source, and any functions earlier in the pipeline still run for them.
Parameters
- n
- How many leading elements to drop.
Returns
A new lazy sequence.
Examples
Skipping a prefix
Seq<int> s = [1, 2, 3, 4, 5].asSeq().skip(3);
console.writeln(s.toArray());
console.writeln([1, 2].asSeq().skip(5).toArray());
[4, 5]
[]
See also: take
take
take(int n) -> Seq<T>Return a lazy sequence of at most the first n elements.
Once n elements have been produced, no further elements are pulled from the source, which
makes take the way to bound a long or expensive pipeline.
Parameters
- n
- The maximum number of elements to produce.
Returns
A new lazy sequence.
Examples
Pulling only what is needed
Seq<int> s = [1, 2, 3, 4, 5].asSeq().map((x) => {
console.writeln("map ${x}");
return x * x;
}).take(2);
console.writeln(s.toArray());
map 1
map 2
[1, 4]
See also: skip
takeWhile
takeWhile((T) => bool pred) -> Seq<T>Return a lazy sequence of the leading elements for which pred is true.
The sequence ends at the first element that fails pred; elements after it are never examined.
Parameters
- pred
- The test applied to each pulled element, in order.
Returns
A new lazy sequence.
Examples
Stopping at the first failure
Seq<int> s = [1, 2, 9, 3].asSeq().takeWhile((x) => x < 5);
console.writeln(s.toArray());
[1, 2]
See also: takeWhile
toArray
toArray() -> Array<T>Run the pipeline and collect every element into an array.
This is a terminal operation: it pulls every element through all the steps. The sequence must be finite.
Returns
A new array of the elements, in order.
Examples
Collecting
Array<int> a = [1, 2, 3, 4].asSeq().where((x) => x % 2 == 1).map((x) => x * 100).toArray();
console.writeln(a);
[100, 300]
where
where((T) => bool pred) -> Seq<T>Return a lazy sequence of the elements for which pred is true.
Nothing is computed until a terminal operation pulls elements.
Parameters
- pred
- The test applied to each pulled element.
Returns
A new lazy sequence.
Examples
Filtering lazily
Seq<int> s = [1, 2, 3, 4, 5].asSeq().where((x) => x > 2);
console.writeln(s.toArray());
console.writeln(s.count());
[3, 4, 5]
3
See also: where
See also
- asSeq — View the array as a lazy
Seq. - The iterator protocol — IIterable and IIterator — Two interfaces that make any type usable in a
for-inloop, and the rules for which loop path the compiler picks. - Array — An ordered sequence of values of one type,
Array<T>, with value semantics. - IIterable — A source of values that can be walked with
for.