Yep, there is immense complexity here, and I do understand the desire to simplify things. But it gets tricky, because there is no One True Way to design a data structure. The Index type and its behavior is extremely closely tied to the exact data structure that the collection implements -- e.g. the differences between how various collections invalidate their indices merely reflect the deep differences between the underlying data structures.
(To add insult to injury, it is generally impossible to implement 100% reliable index validation -- it's the same problem as trying to keep track of the provenance of unsafe pointers, validating that the original buffer is still around and that the access is within bounds on each dereferencing operation. To put it in another way, indices are just pointers in a different disguise. Luckily in this weaker form, pointer mistakes are far less catastrophic.)
For engineers that write code that use collection types, the right thing to do is to always prefer to use (generic or concrete) higher-level algorithms, rather than writing raw loops that operate on raw indices.
For engineers implementing new Collection types, the right thing to do is to emulate the behavior of a standard collection type whenever this is possible. Most Swift programmers know how an Array works, and are able to use it correctly -- so it seems like a good idea for a collection type that could work like an array to actually do work like one, unless there is a really good reason not to do so. I think this is the crux of my argument against RandomAccessCollections with custom indices. (One example for a really good reason would be the UnsafeBufferPointer case.)
So my problem with CircularBuffer's design is that it forces people to learn about its unusual indexing semantics, for some benefit I'm not seeing yet -- what is the really good reason for CircularBuffer to work the way it does?
Ah, I think there is an assumption here that needs to be uprooted and incinerated, to prevent it from spreading even further.
If we're operating on the level of raw indices, we always need to read the documentation. (And only the documentation: the code's behavior may change without warning; promises about index invalidation are forever.)
If we're lucky, the documentation will tell us that indices are simply integer offsets from the start of the collection (or, in the case of subsequence types, the startIndex). In this case, we can stop reading relatively quickly, and simply apply our preexisting knowledge about how arrays work.
In all other cases, we have to be exceedingly careful about not assuming anything about index validation beyond what is promised in the documentation. This means that we need to repeatedly consult the documentation every single time we call a mutating method, interpreting the text in the narrowest possible way, and continuously check and double check that we aren't accidentally holding on to a (potentially) invalidated index.
This is even more important when we're writing code that's generic over one of the mutable collection protocols. For example, if a mutation method does not promise anything about what indices it invalidates, then the only correct assumption is that it will invalidate all of them.
Mistakes can lead to corrupt results rather than a clear trap, because completely reliable index validation is generally impossible/impractical. (And just because an index happens to be valid doesn't mean that it addresses the element we want.)
Again, the best approach is to never deal with raw indices and naked loops.
I don't think this is a particularly helpful position -- for a RandomAccessCollection, it results in an annoyingly pedantic interface, for no good reason whatsoever. I believe it is safe to assume that Swift programmers know how arrays work; there is no need to protect them against the dangers of integer offsets.
The problem with the idea of introducing new syntax to express offset-based indexing in collection types is that I've come to believe that it would simply be a high-effort way to encourage people to write even more code that operates on raw indices, rather than using collection algorithms. (With the added bonus of a potential performance trap.)
I think the effort/resources that would be required to implement new syntax for this would be far better spent on adding even more algorithms, especially ones dealing with pattern matching & replacement. So reviving the offset-based addressing proposal is very low on my personal todo list.
That said, this opinion is somewhat shaken whenever I'm implementing a generic algorithm over RandomAccessCollection and I feel frustrated about the completely pointless verbosity of its indexing operations.
This is why protocols exist -- the best APIs do not take concrete collection types. I expect APIs that insist on e.g. taking an Array will stick out more once other collection types become universally available (and in widespread use). The Collections package will help get to that point, I expect. (String is admittedly somewhat of a special case though.) Well-designed APIs should be convenient to use.
I see where you're coming from, and I agree, but I suspect this may be slightly overstating things. Yep, generic methods often work better when marked @inlinable, but unless one is working on a component where performance is absolutely crucial (like some parts of the stdlib, the System package, or NIO), in most cases the generic implementation can simply convert the argument into an Array and forward the call to an opaque implementation. In many cases, it's also fine to simply expose a non-inlinable generic.
I guess I just don't view slices as anything other than short-term, mostly read-only views. (Mostly read-only, because we cannot currently implement in-place mutations through the range subscript, so slice mutations are riddled with CoW copies. Short-term, because holding onto a slice for too long would be wasting memory.)
To me, the slice being a different type is a feature -- it allows the actual collection to not deal with maintaining (and checking) a separate bounds (which would add a nontrivial cost to every collection).
My proposed guideline is for implementors for collection types, not regular Swift programmers who merely use them. People implementing collections need to be familiar with the collection protocols.
The intended benefit of this guideline for "normal" Swift programmers is that a large percentage of collection types become easier to understand, because they behave in a consistent way, without arbitrary differences, and without hidden gotchas.
As a regular Swift programmer, I find it much easier to follow Array's index invalidation rules than it is to follow RangeReplaceableCollection's. Saying that indices are invalidated on every mutation is emphatically not helpful API design -- it converts what would be straightforward array-like code to become an exercise in tip-toeing across a maze of deadly traps. (This is very well justified in the case of RRC, although we could've done a better job in designing its APIs -- having to lose one's place in the collection every time after inserting an element is not great.)
CircularBuffer's custom indexing rules are halfway between Array and RangeReplaceableCollection, which is even worse. (It is documented to only invalidate indices on size changes, but then it supposedly invalidates all of them. IIUC, the implementation is slightly different, though: if the buffer returns to its original size, then the invalidated indices become valid again -- so as usual with custom indices, the rules aren't fully enforced at runtime.) CircularBuffer entices me to make use of the fact that I can change contents without invalidating indices -- which is a very array-like thing to do -- but then it pulls the carpet from under me by forcing me to regenerate indices after every push and pop. Is all this complexity really worth it?