Making your own types iterable
A bookmark in a book is a tiny iterator: it remembers where you are, and “next” means turn the page. Two people reading the same book each have their own bookmark, so they don’t disturb each other. And you don’t need to know how the book was bound to read it page by page.
In code, an iterator is an object with one job: give the next element, or say there are no more. The collection’s job is to create a new iterator whenever someone wants to loop over it. Every language has a standard shape for this, and when your class follows it, for loops, map, filter and friends just work.
Iterator provides sequential access to an aggregate’s elements without exposing its representation, and separates traversal state from the collection, so several traversals can run independently. Every modern language builds it in: Swift Sequence/IteratorProtocol, Kotlin Iterable/Iterator (and operator fun iterator()), Java Iterable/Iterator (plus Spliterator for streams), TypeScript’s iteration protocol ([Symbol.iterator] and generator functions). Implementing the protocol is nearly always better than exposing internals or inventing getItem(i)/count() APIs: you get the language’s loops and the standard library’s operators for free, and you can change the storage later.
struct RecentSearches: Sequence {
private var buffer: [String?]
private var newest = -1 // index of the newest entry
private(set) var count = 0
init(capacity: Int) { buffer = Array(repeating: nil, count: capacity) }
mutating func add(_ query: String) {
newest = (newest + 1) % buffer.count
buffer[newest] = query
count = min(count + 1, buffer.count)
}
// Each call creates a fresh iterator with its own position
func makeIterator() -> AnyIterator<String> {
var step = 0
return AnyIterator {
guard step < count else { return nil }
let index = (newest - step + buffer.count) % buffer.count
step += 1
return buffer[index]
}
}
}
for query in recent { print(query) } // newest first
let suggestions = recent.filter { $0.hasPrefix(text) }.prefix(5)class RecentSearches(capacity: Int) : Iterable<String> {
private val buffer = arrayOfNulls<String>(capacity)
private var newest = -1
var size = 0
private set
fun add(query: String) {
newest = (newest + 1) % buffer.size
buffer[newest] = query
size = minOf(size + 1, buffer.size)
}
// Each call creates a fresh iterator with its own position
override fun iterator(): Iterator<String> = object : Iterator<String> {
private var step = 0
override fun hasNext() = step < size
override fun next(): String {
if (!hasNext()) throw NoSuchElementException()
val index = (newest - step + buffer.size) % buffer.size
step++
return buffer[index]!!
}
}
}
for (query in recent) println(query) // newest first
val suggestions = recent.filter { it.startsWith(text) }.take(5)public final class RecentSearches implements Iterable<String> {
private final String[] buffer;
private int newest = -1;
private int size = 0;
public RecentSearches(int capacity) { buffer = new String[capacity]; }
public void add(String query) {
newest = (newest + 1) % buffer.length;
buffer[newest] = query;
size = Math.min(size + 1, buffer.length);
}
// Each call creates a fresh iterator with its own position
@Override
public Iterator<String> iterator() {
return new Iterator<>() {
private int step = 0;
public boolean hasNext() { return step < size; }
public String next() {
if (!hasNext()) throw new NoSuchElementException();
int index = (newest - step + buffer.length) % buffer.length;
step++;
return buffer[index];
}
};
}
}
for (String query : recent) System.out.println(query); // newest firstclass RecentSearches implements Iterable<string> {
private readonly buffer: (string | undefined)[];
private newest = -1;
private count = 0;
constructor(capacity: number) { this.buffer = new Array(capacity); }
add(query: string) {
this.newest = (this.newest + 1) % this.buffer.length;
this.buffer[this.newest] = query;
this.count = Math.min(this.count + 1, this.buffer.length);
}
// A generator method: each call returns a fresh iterator
*[Symbol.iterator](): Iterator<string> {
for (let step = 0; step < this.count; step++) {
const index = (this.newest - step + this.buffer.length) % this.buffer.length;
yield this.buffer[index]!;
}
}
}
for (const query of recent) console.log(query); // newest first
const all = [...recent]; // spread works too- The position (
step) lives in the iterator, not in the collection. Two loops overrecentat the same time each have their own position. - The index arithmetic is written once, inside the class. If the storage changes later (a linked list, a database table), the loops outside don’t change.
- Following the language’s protocol gives you the language’s tools:
forloops,filter,prefix/take, spreading into an array. - In TypeScript, a generator (
function*withyield) writes the iterator for you: eachyieldhands out one element and pauses until the loop asks for the next one.
Tap the line with the bug.
var drafts: [Draft] = loadDrafts()
for i in 0..<drafts.count {
if drafts[i].isExpired {
drafts.remove(at: i)
}
}val drafts: MutableList<Draft> = loadDrafts()
for (draft in drafts) {
if (draft.isExpired) {
drafts.remove(draft)
}
}List<Draft> drafts = loadDrafts();
for (Draft draft : drafts) {
if (draft.isExpired()) {
drafts.remove(draft);
}
}const drafts: Draft[] = loadDrafts();
drafts.forEach((draft, i) => {
if (draft.isExpired) {
drafts.splice(i, 1);
}
});Hint: What happens to the loop's position when the list gets shorter under it?
The loop removes elements from the list it’s walking through:
- Kotlin and Java: the list’s iterator notices the change and throws
ConcurrentModificationExceptionon the next step. - Swift: the range
0..<drafts.countwas computed once, so after a removal the loop reaches an index past the end and crashes with “Index out of range”. - TypeScript: no error at all. After
splice, the next element moves into the current index, the loop moves on, and two expired drafts in a row leave the second one behind.
Don’t change a collection while iterating it. Build the result in one step instead, or use the API made for removing while iterating:
drafts.removeAll { $0.isExpired }drafts.removeAll { it.isExpired } // or: val kept = drafts.filterNot { it.isExpired }drafts.removeIf(Draft::isExpired); // or iterator.remove() inside an explicit iterator loopconst kept = drafts.filter((d) => !d.isExpired);Predict the output
Two loops run nested over the same RecentSearches with 3 entries: for a in recent { for b in recent { count += 1 } }. What is count?
Each for asks the collection for a new iterator, so the inner loop starts from the beginning every time and the outer loop's position isn't disturbed: 3 × 3 = 9. If the collection stored a single shared position instead (a common mistake in hand-written iterators), the inner loop would use up the shared position and the outer loop would end after one pass.
- Sequential access without exposing representation; traversal state lives in the iterator, so traversals are independent.
- Built into every language: Swift
Sequence/IteratorProtocol, Kotlin/JavaIterable/Iterator, TS[Symbol.iterator]and generators. - Implementing the protocol buys loops and standard operators and keeps storage private.
- Never mutate during iteration: Java/Kotlin fail fast (
ConcurrentModificationException), Swift crashes on stale indices, TS silently skips. UseremoveAll/removeIf/filteroriterator.remove().
Check this part
Where does the current position of a loop live?
What's the safe way to remove expired items from a list?
TypeScript: what happens when you splice during forEach?