Indexing Sub-Sequences in Swift
December 31, 2024
For a long time, the following behavior in Swift was confusing to me:
let orig = Data([1, 2, 3, 4])
let sub = orig[1...2]
sub[0]
error: Execution interrupted.
For some reason, accessing the first element of the sub-sequence is considered to be out-of-bounds. But in C++ the following code is accepted:
std::vector orig{1, 2, 3, 4};
std::span sub(orig.begin() + 1, 2);
std::cout << sub[0] << std::endl;
And in JavaScript the snippet
orig = [1, 2, 3, 4];
sub = orig.slice(1, 3);
sub[0];
returns the value 2.
It was confusing to me until I saw a sentence
Indices from a slice can be used on the base collection
in Safe Access to Contiguous Storage proposal.
So it means that sub[0]
refers not to the first element in the sub-sequence
but to orig[0]
, which is indeed out-of-bounds for the sub-sequence. But what
indices should we use with a sub-sequence? We have a sub-sequence for a reason;
we don't want to work with the original sequence, don't want to know how its
indices relate to the indices in the sub-sequence. To access the first element
in the sub-sequence we can use Data.startIndex
and make other indices relative
to this one. Understandably, that is more verbose than just 0
, but for
common operations you don't have to write
container[container.startIndex..<container.index(container.startIndex, offsetBy: 4, limitedBy: container.count)]
.
You can use a simpler container.prefix(4)
.
Given the new knowledge of how the sub-sequence indexing works, now the
difference between Array.prefix(Int)
and Array.prefix(upTo: Self.Index)
is
clearer. The [relative] number of an element in a sub-sequence is not the same
as its index. So we have different methods for different cases. Now I find it
extra useful to pay attention if the API works with indices or with the number
of elements. When the underlying index type is Int
it is tempting to treat
both approaches as interchangeable, and it can work for a while. But it can
start failing when you start working with sub-sequences.