Lessons from a Swift Interview
A while back, I interviewed for a role on a SwiftUI-related UI frameworks team at a major platform vendor. Having worked with SwiftUI since its release, I was really excited about this opportunity. But after years as an indie developer, this was my first real technical interview in a while.
Spoiler alert: I didn't get the job. But I did learn something about Swift algorithm design and how to approach technical interviews.
The Assignment
I've used a different example here, but the design lesson is the same.
Implement a function called
adjacentPairs()that yields tuples of consecutive elements, as if it were being added to the Swift Standard Library. Emphasize generality, performance, and safety.
let x = [1, 2, 3, 4, 5]
for (a, b) in x.adjacentPairs() {
print("(\(a), \(b))")
}
// Prints "(1, 2)", "(2, 3)", "(3, 4)", "(4, 5)"
for (a, b) in x.adjacentPairs().reversed().prefix(2) {
print("(\(a), \(b))")
}
// Prints "(4, 5)", "(3, 4)"My Solution
Before starting, I gathered a few references from Swift Algorithms. I decided to model my implementation after UniquedSequence. I created a thin wrapper struct that conforms to Sequence and uses an iterator to track the previous element:
public struct AdjacentPairsSequence<Base: Sequence>: Sequence {
public struct Iterator: IteratorProtocol {
var base: Base.Iterator
var previous: Base.Element?
public mutating func next() -> (Base.Element, Base.Element)? {
if previous == nil {
previous = base.next()
}
guard let prev = previous, let current = base.next() else {
return nil
}
previous = current
return (prev, current)
}
}
// ...
}This worked correctly. The adapter is created in O(1), and pairs are produced lazily as you iterate. I was pretty happy with my solution.
The Follow-Up Question
The next question was about the performance of this code:
let x = sequence(first: 1, next: { $0 < 1_000_000 ? $0 + 1 : nil })
for (a, b) in x.adjacentPairs().reversed().prefix(2) {
print("(\(a), \(b))")
}My answer:
The code will struggle to evaluate mainly due to the application of
reverse()on aSequence. This will force the evaluation of the whole sequence, which was probably intended to be lazy. As far as I can tell, there is no way to efficiently get to the last elements of a forward-only sequence since it has to evaluate itself forward until it reaches the end condition. Furthermore, there doesn't seem to be a way to applyreverse()in a lazy manner unless the operation is specifically on a collection with bidirectional indices.
Everything here is correct. What tripped me up is that there's no way for a generic algorithm to make a forward-only sequence efficient in this case. I also knew that uniqued() would have the same issue. So it felt like an inherent limitation, not something my design could have avoided. This left me a bit uncertain as to whether the interviewer was testing my ability to analyze performance, or probing for a flaw in my design.
Turns out it was the latter. I missed the key insight: my implementation could have been a BidirectionalCollection.
The Key Insight: Sequence vs Collection
I had modeled my solution after UniquedSequence, which only conforms to Sequence. But there's a reason for that design choice, and it didn't apply to my algorithm.
Why uniqued() is Sequence-only
The uniqued() algorithm tracks all previously seen elements in a Set:
internal var seen: Set<Subject> = []This accumulated state makes Collection conformance impractical. To implement index(before:) for backward traversal, you'd need to know the "seen" set at every position, which would require traversing from the start.
Why adjacentPairs can be a Collection
My algorithm only needs to look at adjacent indices. No accumulated state required:
- Pair at index
iis just(base[i], base[i+1]) - Going backwards: pair at index
i-1is(base[i-1], base[i]) - No history, no Set, just index arithmetic
This is structurally similar to ChunkedByCollection from Swift Algorithms, which does conform to Collection and BidirectionalCollection.
The Performance Difference
| Operation | Sequence-only | With BidirectionalCollection |
|---|---|---|
adjacentPairs() | O(1) | O(1) |
.reversed() | O(n) - materializes array | O(1) - returns lazy ReversedCollection |
.prefix(2) | O(1) | O(1) |
Total for .reversed().prefix(k) | O(n) time + O(n) memory | O(k) time, O(1) memory |
With BidirectionalCollection conformance, reversed() returns a lazy ReversedCollection wrapper. No O(n) allocation, and you only compute the pairs you actually need.
What I Should Have Done
If the base type is a Collection, my wrapper should also be a Collection. If it's a BidirectionalCollection, mine should be too:
extension AdjacentPairsCollection: Collection
where Base: Collection
{
// Index is just Base.Index
// startIndex = base.startIndex
// endIndex = base.index(before: base.endIndex)
// subscript: return (base[i], base[index(after: i)])
}
extension AdjacentPairsCollection: BidirectionalCollection
where Base: BidirectionalCollection
{
func index(before i: Index) -> Index {
base.index(before: i)
}
}The iterator-based Sequence implementation still works for arbitrary sequences. But for collections, the additional conformances enable lazy composition with operations like reversed().
Lessons Learned
1. Different algorithms have different conformance requirements. I modeled my solution after uniqued(), but that algorithm has fundamentally different state requirements. I should have recognized that my algorithm didn't need accumulated state, and could therefore conform to Collection.
2. Trust first principles over references. I've designed plenty of APIs for my own projects, but nothing at the level of the Swift Standard Library. So I leaned heavily on existing solutions as a guide. I should have focused on what my problem actually required rather than assuming uniqued() was the right template.
3. Think hard about composition. Standard Library design isn't just about the algorithm itself, it's about how it composes with other operations like reversed(), prefix(), lazy, etc. This way of thinking was pretty new to me, coming from a background of building apps, not low-level APIs.
4. Interview questions often have layers. The follow-up questions weren't really about identifying performance issues, they were asking "how could your design have avoided this?". While I could sense that I was missing something obvious, the pressure of the interview got to me and I didn't do a good enough job at recognizing the implicit question in the moment.
5. Do more interviews. I was given ample opportunity to reason my way out of the flaw during the interview, but I locked up once I realized something was wrong. This was my first technical interview in many years, and my nerves got the better of me.