MutableCollection allows length-changing operations

It has recently come to my attention that the slicing subscript on MutableCollection is { get set }. As in, it can be assigned to.

The documentation for MutableCollection states:

However, since slices are settable, we can currently write code like this:

extension MutableCollection {
  mutating func swapSubranges<R: RangeExpression, S: RangeExpression>(_ a: R, _ b: S)
    where R.Bound == Index, S.Bound == Index
  {
    (self[a], self[b]) = (self[b], self[a])
  }
}

This compiles, and can be called with ranges of different length:

var x = [1, 2, 3, 4]
x.swapSubranges(0...0, 2...)
print(x)    // [3, 4, 1]         It got shorter!

Note that the “2” was overwritten, despite being located outside both subranges, and the length of the collection changed despite calling only MutableCollection methods.

In fact we can even make it longer than it was originally:

x.swapSubranges(0...1, 2...)
print(x)    // [1, 1, 3, 4]
x.swapSubranges(0...1, 3...)
print(x)    // [4, 3, 4, 1, 1]

Is this behavior known?

Is it intentional?

Should the the slicing subscript be get-only here?

The way I think we should think about this is: it might work, and in this case it does work because Array implements RangeReplaceableCollection, but it isn't guaranteed to work for any MutableCollection. If a type decided to fatalError or preconditionFailure out instead, that would be a valid implementation.

I think we should repeat the notice about not changing the length in the subscript's documentation, though. Having it on the definition for MC isn't sufficient, because a user of a conforming type won't see that on any visible members in an IDE.

2 Likes

It's not MutableCollection, it's Array that doesn't behave as expected.
The following traps:

var x = [1, 2, 3, 4]
x.withUnsafeMutableBufferPointer {
  (buffer) -> Void in
  buffer.swapSubranges(0...0, 2...)
}

Array's range-of-indices subscripts calls out to replaceSubrange, which is from the RangeReplaceableCollection protocol -- explains this behaviour.

The behavior is immaterial.

My point is that the slicing subscript on MutableCollection allows assignment at all.

Either it should be read-only because it cannot guarantee the length doesn’t change, or it should be LOUDLY documented that it only supports assigning slices of the same size.

If the range-of-indices subscript didn't have a setter, you couldn't sort a slice from a MutableCollection. That would be very unfortunate. Incidentally, sorting a slice of a MutableCollection is a motivating example for the protocol in its overview.

Said overview ends with:

The MutableCollection protocol allows changing the values of a collection’s elements but not the length of the collection itself. For operations that require adding or removing elements, see the RangeReplaceableCollection protocol instead.

I agree with Karl that that warning should also appear in the subscript's documentation.

It seems to me that Array's subscript implementation errs in calling out to replaceSubrange, but fixing that (regardless of desirability) would be quite source-incompatible.
RRC declares the range-of-indices subscript as well, so it makes sense that Array's should call out to replaceSubrange.

1 Like

Array's subscript is fine. Nothing says that a MC has to fail when changing the length; the burden is on users of an MC not to do so. Array happens to be lenient rather than failing.

EDIT: Ah, never mind.

1 Like

Thanks for flagging this! I opened [SR-10030] Mutable collection ranged subscript docs should mention length requirement · Issue #52433 · apple/swift · GitHub to track the documentation issue.

2 Likes

I feel that MutableCollection's range subscript requires the source sequence to have the same length as the slice, and it's undefined-behavior/precondition-failure otherwise. This is suspended if the collection conforms to RangeReplaceableCollection too, since then the receiver has the option to resize. (Array conforms to both.)