Array is one concrete container. Sequence and Collection describe capabilities shared by many containers and producers. An algorithm that only needs to iterate can accept a Sequence; one that needs stable indices or repeated access may need Collection or a stronger protocol.
The arrays lesson teaches everyday operations. This guide explains the contracts behind them and develops lazy processing beyond a brief syntax change. Choosing a general abstraction should preserve the assumptions your algorithm actually needs, not merely make its type signature look more flexible.
Iteration is weaker than collection access
A Sequence supplies an iterator that produces elements until it returns nil. Not every sequence promises repeatable independent traversal, finite length or indexed access. A source reading a stream can naturally consume data as it goes. Do not loop twice over an arbitrary Sequence and assume both passes produce identical values.
Collection adds an indexed, multipass model. BidirectionalCollection supports stepping backward, and RandomAccessCollection adds efficient arbitrary index movement under its contract. These refinements let generic algorithms state their real needs rather than assume that every collection behaves exactly like an Array.
Use indices instead of hard-coded integer positions in generic collection code. A collection's Index need not be Int, and even an integer-indexed slice may not start at zero. EndIndex is the position after the last element and is not itself a valid element index. An empty collection has no first element despite having valid boundary indices.
If you implement a custom Sequence, its iterator owns traversal state and advances that state in next. Test exhaustion explicitly: once the iterator has ended, later next calls must continue returning nil. Obtain independent iterators through makeIterator rather than assuming a copied iterator has independent underlying resources. These small contract tests make a custom producer predictable for generic algorithms that never inspect its concrete representation.
Lazy work is deferred, not free
Lazy map and filter can avoid intermediate arrays and stop when the consumer has enough results. A terminal operation such as Array(pipeline) requests all available results for a finite source and allocates the requested final array. Sorting needs information about the full input and cannot globally order an endless arbitrary source.
Reading a lazy computed element can repeat work depending on the wrapper and access pattern. Materialize a bounded result when several consumers need the same expensive computation exactly once. A mutable or single-use source also affects repeatability; laziness is not an immutable snapshot or an automatic memoization promise.
For small inputs, eager transformations can be simpler and faster than layered lazy iteration. Benchmark equivalent completed operations with realistic data and consumers. Measuring only the construction of a deferred recipe does not measure the work it will eventually perform. Keep semantics and readability ahead of unsupported performance claims.
Slices preserve useful indices and may retain storage
An ArraySlice represents a region of an array and preserves its original indices. If a slice begins at index two, slice[0] is not a safe way to request its first element. Use first, startIndex, indices or index movement according to the operation you need.
Preserved indices help relate a subrange back to its original collection. They can surprise APIs expecting a fresh zero-based array, so convert with Array(slice) when independent zero-based storage is the actual contract. That conversion is an intentional copy or materialization boundary, not merely a cosmetic type annotation.
A slice can retain a larger backing allocation than its visible portion. Holding a tiny slice for a long time may keep substantial original storage alive. If the selected data must outlive the original processing phase, an independent array can be appropriate. Consider element reference semantics too: copying the container does not deep-copy class instances it contains.
Official references
The Sequence documentation and Collection documentation define their capabilities. The official Sequence source includes traversal contracts. Review finite input, repeatability, index validity, retained storage and integer overflow separately; none is established merely by calling a value a collection.
Swift Sequence, Collection and Lazy Algorithms Examples
Each example shows the code, the output it produces and what each part does.
Trace a lazy short-circuiting pipeline
Using the iterator directly makes the requested work clear: the program asks for only two results. The lazy wrappers do not materialize every stage at construction. Prefer pure transforms in production so changing evaluation strategy does not change unrelated observable state.
let pipeline = [1, 2, 3, 4, 5].lazy
.filter { value in
print("check:\(value)")
return value.isMultiple(of: 2)
}
.map { value in
print("map:\(value)")
return value * 10
}
print("ready")
var iterator = pipeline.makeIterator()
print(iterator.next() ?? -1)
print(iterator.next() ?? -1)ready
check:1
check:2
map:2
20
check:3
check:4
map:4
40Swift Sequence, Collection and Lazy Algorithms in Real Projects
Worked exercise: Sum the first accepted quantities
Write a generic Sequence algorithm that takes at most three positive quantities and sums them without requiring indexed access or a second pass. Ignore invalid nonpositive values and stop when the accepted count reaches three.
func firstPositiveTotal<S: Sequence>(_ values: S, limit: Int = 3) -> Int where S.Element == Int {
guard limit > 0 else { return 0 }
var accepted = 0
var total = 0
for value in values {
guard value > 0 else { continue }
total += value
accepted += 1
if accepted == limit { break }
}
return total
}
print(firstPositiveTotal([0, 2, -1, 4, 3, 99]))
let slice = [8, 0, 2, 4, 6][2...4]
print(slice.startIndex)
print(firstPositiveTotal(slice, limit: 2))
print(firstPositiveTotal([Int]()))9
2
6
0The algorithm works for the slice even though its first index is two, because it only iterates. For untrusted values with large totals, add an explicit overflow policy or use an appropriate wider representation. This example uses small safe values and focuses on capability selection rather than claiming every sum fits Int.
Swift Sequence, Collection and Lazy Algorithms FAQ
Can I traverse every Sequence twice?
No. Sequence is an iteration capability and does not universally promise a reusable independent stream. Some sources consume data or produce different values over time. Collection provides the multipass indexed contract when that is required. If your algorithm needs a second pass over an arbitrary bounded sequence, materialize it once and document the memory cost. Avoid counting a stream first and then assuming the same elements remain for later processing.
Why does an ArraySlice start at a nonzero index?
Slices preserve the original collection's indices so positions remain meaningful relative to their source. This is useful for subrange algorithms but differs from a newly created Array. Access the first element with first or startIndex, and iterate through indices when positions matter. Convert to Array when a separate zero-based container is required. Assuming index zero exists can trap even when the slice visibly contains several elements.
Does lazy cache every mapped value?
No. Laziness defers work and allows demand-driven processing; it is not a blanket memoization promise. A particular access pattern can reevaluate transformations, and traversing a repeatable lazy source again normally performs work again. Materialize results when you need an explicit stable snapshot or one completed expensive computation. This is especially important when a transform has external effects, although pure transforms are easier to reason about in either evaluation strategy.
Should a generic function require RandomAccessCollection?
Only if it actually needs that capability or its complexity guarantee. A function that simply loops once can accept Sequence, while stable indexed multipass processing may need Collection. Requiring a stronger protocol excludes useful inputs unnecessarily. Conversely, accepting a weaker protocol and then assuming constant-time arbitrary index movement misstates the algorithm's contract. Name the needed operations and complexity assumptions before selecting the generic constraint.
Swift Sequence, Collection and Lazy Algorithms Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
An algorithm only visits elements once and stops after enough matches. Which broad constraint fits?
Swift Sequence, Collection and Lazy Algorithms Quiz
2 questions to check what you learned.
What is not universally promised by Sequence alone?
- Repeated identical traversal and indexed random access.
- An element type.
- Producing elements through an iterator.
Why is slice[0] unsafe for an arbitrary ArraySlice?
- Slices always convert integers into strings.
- Its indices may start at an original nonzero position.
- Every slice is empty.