>> (On the other hand, it might be that I'm conceiving of the purpose of >> `limitedBy` differently from you—I think of it as a safety measure, but you >> may be thinking of it specifically as an automatic truncation mechanism.) > > Hi Brent, > > Could you explain what kind of safety do you have in mind? Swift will > guarantee memory safety even if you attempt to advance an index past > endIndex using the non-limiting overload.
By "safety" here, I mean what I will call "index safety": not accidentally using an index which would violate the preconditions of the methods or properties you are planning to use it with. I think it's too easy to accidentally overrun the permitted range of indices, and the API should help you avoid doing that. For instance, suppose I'm porting XCTest to Swift, and I decide to rewrite its `demangleSimpleClass` function, which extracts the identifiers from a mangled Swift symbol name. Specifically, I'm implementing `scanIdentifier`, which reads one particular identifier out of the middle of a string. (For those unfamiliar: an identifier in a mangled symbol name consists of one or more digits to represent a length, followed by that many characters.) I will assume that the mangled symbol name is in a Swift.String. Here's a direct port: func scanIdentifier(partialMangled: String) -> (identifier: String, remainder: String) { let chars = partialMangled.characters var lengthRange = chars.startIndex ..< chars.startIndex while chars[lengthRange.endIndex].isDigit { lengthRange.endIndex = chars.successor(of: lengthRange.endDigit) } let lengthString = String(chars[lengthRange]) let length = Int(lengthString)! let identifierRange = lengthRange.endIndex ..< chars.index(length, stepsFrom: lengthRange.endIndex) let remainderRange = chars.suffix(from: identifierRange.endIndex) return (String(chars[identifierRange]), String(chars[identifierRange])) } This works (note: probably, I haven't actually tested it), but it fails a precondition if the mangled symbol is invalid. Suppose we want to detect this condition so that our parent function can throw a nice error instead: func scanIdentifier(partialMangled: String) -> (identifier: String, remainder: String)? { let chars = partialMangled.characters var lengthRange = chars.startIndex ..< chars.startIndex while chars[lengthRange.endIndex].isDigit { lengthRange.endIndex = chars.successor(of: lengthRange.endDigit) if lengthRange.endIndex == chars.endIndex { return nil } } let lengthString = String(chars[lengthRange]) guard let length = Int(lengthString) else { return nil } let identifierRange = lengthRange.endIndex ..< chars.index(length, stepsFrom: lengthRange.endIndex) if identifierRange.endIndex > chars.endIndex { return nil } let remainderRange = chars.suffix(from: identifierRange.endIndex) return (String(chars[identifierRange]), String(chars[identifierRange])) } That's really not the greatest. To tell the truth, I've actually guessed what bounds-checking is needed here; I'm not 100% sure I caught all the cases. And, um, I'm not really sure that `index(length, stepsFrom: lengthRange.endIndex)` is guaranteed to return anything valid if `length` is too large. Even `limitedBy:` wouldn't help me here—I would end up silently accepting and truncating an invalid string instead of detecting the error. Now, imagine if `successor(of:)` and `index(_:stepsFrom:)` instead had variants which performed range checks on their results and returned `nil` if they failed: func scanIdentifier(partialMangled: String) -> (identifier: String, remainder: String)? { let chars = partialMangled.characters var lengthRange = chars.startIndex ..< chars.startIndex while chars[lengthRange.endIndex].isDigit { guard let nextIndex = chars.successor(of: lengthRange.endDigit, permittingEnd: false) else { return nil } lengthRange.endIndex = nextIndex } let lengthString = String(chars[lengthRange]) guard let length = Int(lengthString) else { return nil } guard let identifierEndIndex = chars.index(length, stepsFrom: lengthRange.endIndex, permittingEnd: true) else { return nil } let identifierRange = lengthRange.endIndex ..< identifierEndIndex let remainderRange = chars.suffix(from: identifierRange.endIndex) return (String(chars[identifierRange]), String(chars[identifierRange])) } By using these variants of the index-manipulation operations, the Collection API itself tells me where I need to handle bounds-check violations. Just like the failable `Int(_: String)` initializer, if I forget to check bounds after manipulating an index, the code will not type-check. That's a nice victory for correct semantics. * * * Incidentally, rather than having Valid<Index>, an alternative would be to have Unchecked<Index>. This would mark an index which had *not* been checked. You could use its `uncheckedIndex` property to access the index directly, or you could pass it to `Collection.check(_: Unchecked<Index>) -> Index?` to perform the check. This would not serve to eliminate redundant checks; it would merely get the type system to help you catch index-checking mistakes. You could, of course, perform the check and then invalidate the index with a mutation, but that's just as true today. I believe that, with aggressive enough optimization, this could be costless at runtime. *And* it would offer a way to provide the so-called "safe indexing" many people ask for: you could offer a subscript which took an Unchecked<Index> and returned an Optional<Element>. -- Brent Royal-Gordon Architechies _______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution