The criteria for adding something as an API for the standard library is not "is it a low-level building block for some other operation". Sometimes those building blocks are, themselves, also needed to create multiple different higher-level operations, so they merit adding on their own. But sometimes they aren't useful except to build this one thing, and in that case, they shouldn't be added as public API.
I don't think your example of swapping two random elements in a collection is something that needs to be done often (or ever?). On the other hand, if you take the very common case of shuffling an entire collection, it wouldn't be particularly useful to pick a random index from the collection. To implement something like the fisher-yates shuffle, you need to move progressively through the collection, swapping random elements in a narrowing range. Having a randomIndex
operation would not be a significant readability improvement over just advancing the left side of that narrowing range by a random distance. And even if it were, it would require a slicing operation that would lean on the optimizer to make efficient.
Adding API also needs to consider the "do no harm" angle. I suspect that the most common use of getting a random index would be misuse in cases where the user wants "without replacement" functionality:
while let idx = cards.randomIndex() {
deal(cards[idx])
cards.remove(at: idx)
}
Of course, if a feature is genuinely useful, we should add it even if it can be misused—hence we have firstIndex(where:)
. But we shouldn't add a non-useful API that invites misuse.
edit: I thought of a compelling use case for a random index: slicing a collection into two randomly-sized parts. If other common uses come up it would weigh more in favor of adding the index version despite the risks.
edit 2: thinking about that some more, maybe not so great, as it reveals another hidden gotcha: can the randomIndex
include the endIndex
or not? Unlike firstIndex(where:)
that precludes that possibility, including it might sometimes be what you want. For example, does this code ever return an empty index for the first half, second half, or neither, or both? It's not obvious without reading the docs for randomIndex
:
let r = a.randomIndex()!
let fst = a[..<r]
let snd = a[r...]