Compiler lets me use incomplete RangeReplaceableCollection

The documentation clearly lists some required methods, yet the Swift (4.2, Xcode 10.1) compiler doesn't complain, and lets me get away without implementing them.

Shouldn't the compiler save me from my stupidity? (More seriously, I depend on the compiler to force me to add the bare minimum of required methods. It didn't, here. Also, the documentation for RangeReplaceableCollection says in the synopsis that only one particular new method is required, but there are additional methods further down which say required as well. Are they really? Again, depending on the compiler to help me out.)

Here's the world's stupidest vector class, and Swift lets me use it. Here's some test code. It prints correctly, but then goes into an infinite loop when you call append (I assume a consequence of Swift doing something behind my back and/or me breaking the rule about empty collections or something.)

    var v = StupidVector()
    for value in v {
         print("Got: ", value)
    }
    v.append(23.0)     // enters infinite loop here...

Here's the code for StupidVector. Again, it should be illegal since I've left stuff out, but the compiler cheerfully lets me hang myself:

public struct StupidVector : RangeReplaceableCollection {
    public typealias Index = Int
    public typealias Element = Double

    public init() {
    }

    public func index(after i: Int) -> Int {
        return i + 1
    }
    
    public subscript(position: Int) -> Double {
        return Double(position)
    }
    
    public struct Iterator : IteratorProtocol, Comparable {
        public typealias Element = StupidVector.Element
        var position = 0
        
        public mutating func next() ->  Element? {
            position += 1
            return position < 10 ? Element(position) : nil
        }
        
        public static func < (_ lhs: Iterator, _ rhs: Iterator) -> Bool {
            return lhs.position < rhs.position
        }
        
        public static func == (_ lhs: Iterator, _ rhs: Iterator) -> Bool {
            return lhs.position == rhs.position
        }
    }
    
    public func makeIterator() -> Iterator {
        return Iterator()
    }

    public var startIndex: Index {
        return 0
    }
    
    public var endIndex: Index {
        return 10
    }
}
2 Likes

A very interesting case, thanks for bringing this forward! The documentation states that replaceSubrange is required, but for some reason also has a default implementation. Since you are getting an infinite loop, the latter appears to be formally true, though I suspect it was actually meant to be an overload rather than a covariant witness. The type of the implemented method is a subtype of the requirement type, but more importantly, the "default implementation" simply calls the requirement, which doesn't make sense unless the original intention was an overload.
So in a nutshell, the compiler is erroneously treating as a default implementation what was meant to be an overload, and that is why you're getting the infinite loop (append -> insert -> replaceSubrange -> ∞) and zero objections.

protocol P {}
class A: P {}

protocol Foo {
    mutating func foo(arg: A)
}

struct Test: Foo {
// This one is currently a valid witness.
    mutating func foo<R>(arg: R) where R: P {}

//  This one is not   
//    mutating func foo(arg: P) {}
}

The solution... is to implement replaceSubrange and file a bug to track the issue.

Good find! That default implementation has the first parameter generic over RangeExpression, so that you can write something like myArray.replaceSubrange(..<5, with: [10, 11, 12]), and tries to call through to the requirement, which specifically has a Range<Index> for that parameter. Unfortunately, that generic version satisfies the non-generic requirement, so the compiler doesn't help with an error message. We have some other areas where this happens too, due to default implementations that are more expansive than the protocol requirements.

The protocol documentation has a section on conforming to the protocol, with an explanation of why only a couple of the "requirements" are actually required:

To add RangeReplaceableCollection conformance to your custom collection, add an empty initializer and the replaceSubrange(_:with:) method to your custom type. RangeReplaceableCollection provides default implementations of all its other methods using this initializer and method. For example, the removeSubrange(_:) method is implemented by calling replaceSubrange(_:with:) with an empty collection for the newElements parameter. You can override any of the protocol’s required methods to provide your own custom implementation.

The section you note is quite explicit: what I found confusing was that as I scrolled down, there were several things marked “required” (but that did not “default implementation”) that were not, in fact, required. (Or, at least, that did not require me to write them, which is what I care about).

Indices (plural) springs to mind, as well as one of the subcript methods (probably on a Range or Indices).

It just becomes a matter of not knowing which documentation is correct, so I figured I’d let the compiler be the final arbitrator! :)

Note that at this point (I use “dash” for documentation) there is both the Swift docset for stuff and the Apple docset. They conflict slightly on things like this protocol. (Apple notes that replaceSubrange is required and default implemented. The Swift version does not.)

@nnnnnnnn Are you roughly aware since when do we have this ability to make witnesses "covariant over generic parameters"?

This is a known "issue" (I'm not too sure if that's what they consider it, or if it's intended behavior), but there are other places in the stdlib where this occurs.

My hunch is that it's been around as long as default implementations have been a feature. The issue specific to RangeReplaceableCollection dates to the introduction of RangeExpression, which was Swift 4 IIRC.

2 Likes

Quite unexpected, if that's the case. I really did overreact :sweat_smile: