Virtual Sequences
Monday, October 5, 2026
Factor has had virtual sequences for a long time. A virtual sequence is typically lazy and can compute an element when asked for it, or look it up in another sequence, rather than build all the elements up front. It only needs to provide the sequence operations that describe its contents.
That lets us use normal sequence words
like nth, each, map, and reduce with ranges,
reversed views,
windows into arrays,
and even collections of combinations.
I thought it would be fun to look at the different kinds we have accumulated over the years.
Note: In the examples below, >array materializes the elements into an array
so the printed output shows their contents. >string does the same for
character sequences. In some of the examples, the elements are themselves
views like slices, we use [ >array ] map to materialize each one.
These conversions are just for display; words such as nth, each,
map, etc. can work directly with the virtual sequence.
Computed elements
We can start with sequences whose elements follow a simple formula:
USING: arrays prettyprint ranges sequences ;
IN: scratchpad 5 <iota> >array .
{ 0 1 2 3 4 }
IN: scratchpad 2 10 2 <range> >array .
{ 2 4 6 8 10 }
IN: scratchpad 4 "hello" <repetition> >array .
{ "hello" "hello" "hello" "hello" }
An iota stores its length, a range stores the information needed to
compute its arithmetic progression, and a repetition stores a length and
one element. Repeating a mutable object repeats the reference to that
object.
The ranges vocabulary
also has convenient constructors such as [a..b], [a..b), and
[1..b] for inclusive and exclusive endpoints. And
math.bits
lets us treat an integer as a sequence of bits:
USE: math.bits
IN: scratchpad 0b10110 5 <bits> >array .
{ f t t f t }
Here the least significant bit comes first. <binary-bits> provides the
same ordering using 0 and 1 instead of booleans.
Selecting and rearranging elements
Many virtual sequences are views of existing data. Two particularly useful
ones, <slice> and <reversed>, are part of the core
sequences vocabulary:
IN: scratchpad 1 4 { 10 20 30 40 50 } <slice> <reversed> >array .
{ 40 30 20 }
The slice selects indices 1 through 3, and the reversed view reads those indices in the opposite order. Neither view copies the elements. They also support writing through to a mutable backing sequence:
USE: kernel
IN: scratchpad { 10 20 30 } clone
dup <reversed> 99 0 rot set-nth .
{ 10 20 99 }
Changing index zero of the reversed view changes the last element of the original array.
There are several other ways to select or rearrange existing elements:
| Vocabulary | Constructors | View |
|---|---|---|
| sequences.extras | <evens>, <odds>, <step-slice> |
Select even or odd indices, or indices separated by a step |
| sequences.snipped | <snipped>, <removed> |
Skip a span or one element |
| sequences.rotated | <rotated> |
Read from a different starting position, wrapping around |
| circular | <circular> |
Wrap a sequence with a movable starting position |
| columns | <column> |
Read one column of a sequence of rows |
For example, a step of two selects every other element (equivalent to [::2] in Python):
USE: sequences.extras
IN: scratchpad f f 2 { 10 20 30 40 50 } <step-slice> >array .
{ 10 30 50 }
The two f values select the default start and end. A column view picks
an element from each row:
USE: columns
IN: scratchpad { { 1 2 3 } { 4 5 6 } } 1 <column> >array .
{ 2 5 }
The related <flipped> word builds a sequence of column views, giving us
a transposed view of a rectangular matrix. It allocates the outer
collection of views while sharing the row data.
Groups and windows
Sometimes the elements of a virtual sequence are themselves views. The grouping vocabulary provides non-overlapping groups and overlapping clumps:
USE: grouping
IN: scratchpad { 1 2 3 4 5 } 2 <groups> [ >array ] map .
{ { 1 2 } { 3 4 } { 5 } }
IN: scratchpad { 1 2 3 4 5 } 3 <clumps> [ >array ] map .
{ { 1 2 3 } { 2 3 4 } { 3 4 5 } }
A group can be shorter at the end; a clump has the requested size. Their
elements are slices, which is why these examples convert each element to
an array. The <circular-slice> and <circular-clumps> constructors
extend the idea to windows that wrap around the end.
The sequences.windowed vocabulary has trailing windows, including shorter ones at the beginning:
USE: sequences.windowed
IN: scratchpad { 1 2 3 4 5 } 3 <windowed-sequence>
[ >array ] map .
{ { 1 } { 1 2 } { 1 2 3 } { 2 3 4 } { 3 4 5 } }
And grouping.extras
provides <prefixes> and <suffixes> for the nonempty initial and final
slices of a sequence:
USE: grouping.extras
IN: scratchpad { 1 2 3 } <prefixes> [ >array ] map .
{ { 1 } { 1 2 } { 1 2 3 } }
IN: scratchpad { 1 2 3 } <suffixes> [ >array ] map .
{ { 1 2 3 } { 2 3 } { 3 } }
Repeating and combining sequences
We can make a sequence appear longer by reusing its elements. The sequences.repeating vocabulary offers two different arrangements:
USING: sequences.repeating strings ;
IN: scratchpad "abc" 8 <cycles> >string .
"abcabcab"
IN: scratchpad "abc" 3 <element-repeats> >string .
"aaabbbccc"
<cycles> takes the desired total length. <cycles-from> also accepts
a starting offset. <element-repeats> takes the number of times to repeat
each element.
Other views assemble data from multiple sources:
| Vocabulary | Constructors | Result |
|---|---|---|
| sequences.cords | cord-append |
Concatenation that retains its two input sequences |
| sequences.merged | <merged>, <2merged>, <3merged> |
Alternating elements from the inputs, stopping at the shortest input |
| sequences.zipped | <zipped> |
Pairs of corresponding elements, stopping at the shorter input |
| sequences.interleaved | <interleaved> |
A separator element between adjacent elements |
| sequences.prefixed, sequences.suffixed | <prefixed>, <suffixed> |
One extra element at the beginning or end |
| sequences.padded | <padded-head>, <padded-tail> |
Fill elements extending a sequence to a minimum length |
| sequences.shifted | <shifted> |
A shifted view of the same length, with a fill element in exposed positions |
Here are a few examples:
USING: sequences.cords sequences.interleaved sequences.merged
sequences.padded sequences.zipped ;
IN: scratchpad { 1 2 } { 3 4 } cord-append >array .
{ 1 2 3 4 }
IN: scratchpad { 1 2 3 } { 10 20 30 40 } <2merged> >array .
{ 1 10 2 20 3 30 }
IN: scratchpad { 1 2 3 } { 10 20 } <zipped> >array .
{ { 1 10 } { 2 20 } }
IN: scratchpad "abc" CHAR: - <interleaved> >string .
"a-b-c"
IN: scratchpad { 1 2 3 } 5 0 <padded-head> >array .
{ 0 0 1 2 3 }
The assocs vocabulary
also provides <enumerated>, a view of index/value pairs that works as
both a sequence and an association. <zip-index> in sequences.extras
provides a sequence of index/value pairs as well.
Transforming values
Views can transform values as they are read. The sequences.modified vocabulary provides scaling, offsets, and elementwise sums:
USE: sequences.modified
IN: scratchpad { 1 2 3 } 10 <scaled> 1 <offset> >array .
{ 11 21 31 }
IN: scratchpad { { 1 2 3 } { 10 20 } } <summed> >array .
{ 11 22 3 }
The summed view extends to the longest input, treating missing elements as zero. Scaled and offset views can also translate writes back to their underlying sequence; writing through a scaled view requires a nonzero scale factor.
For complex numbers, sequences.complex interprets adjacent real values as real and imaginary components, while sequences.complex-components exposes those components from a sequence of complex numbers.
Products and combinations
Some virtual sequences represent collections that could be much larger than their inputs. The sequences.product vocabulary provides a Cartesian product:
USE: sequences.product
IN: scratchpad { { "red" "blue" } { 1 2 3 } }
<product-sequence> >array .
{
{ "red" 1 }
{ "red" 2 }
{ "red" 3 }
{ "blue" 1 }
{ "blue" 2 }
{ "blue" 3 }
}
Similarly, math.combinatorics
provides <permutations>, <k-permutations>, and <combinations>:
USE: math.combinatorics
IN: scratchpad { "a" "b" "c" } 2 <combinations> >array .
{ { "a" "b" } { "a" "c" } { "b" "c" } }
IN: scratchpad { "a" "b" "c" } <permutations> length .
6
These can compute a selected result without constructing every result
before it. Iterating over the entire sequence still takes work proportional
to the number of results, and >array asks to store all of them.
Sharing data and materializing results
A view shares data with its source. Even sequences.frozen, which prevents writes through the view, can observe changes made through the original array:
USE: sequences.frozen
IN: scratchpad { 1 2 3 } clone dup <frozen>
swap 99 0 rot set-nth >array .
{ 99 2 3 }
Keeping a small slice can therefore keep a much larger backing sequence alive. Nested views add indexing work, and some views allocate an element, such as a pair or a slice, when it is requested.
Implementing a virtual sequence
There is a useful implementation detail behind these examples. The core
virtual-sequence mixin supports views that translate an index into an
index and another sequence using virtual@. A wrapped-sequence
already supplies a backing sequence and delegates its length. Reversing
one takes very little code:
USING: accessors kernel math sequences ;
TUPLE: backwards < wrapped-sequence ;
C: <backwards> backwards
M: backwards virtual@
seq>> [ length swap - 1 - ] keep ;
virtual-exemplar supplies the sequence used to choose result types for
operations such as map. Computed sequences, such as ranges and
combinations, can instead implement the sequence protocol directly. The
broader idea of a virtual sequence covers both approaches.
The same machinery appears in specialized structures: composed quotations in quotations, strided storage in arrays.shaped, BLAS vectors in math.blas.vectors, gap buffers, and insertion views in sequences.inserters.
Have ideas for other virtual sequences that would be useful in Factor? Let us know!