Why does count return different types for Collection vs. Array?

arrays, associated-types, collections, count, swift

Solution

From Associated Types in the Swift Programming Language (emphasis added):

When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.

In Swift 3/4.0, the `Collection` protocol defines five associated types (from What’s in a Collection?):

protocol Collection: Indexable, Sequence {
    associatedtype Iterator: IteratorProtocol = IndexingIterator<Self>
    associatedtype SubSequence: IndexableBase, Sequence = Slice<Self>
    associatedtype Index: Comparable // declared in IndexableBase
    associatedtype IndexDistance: SignedInteger = Int
    associatedtype Indices: IndexableBase, Sequence = DefaultIndices<Self>
    ...
}

Here

    associatedtype IndexDistance: SignedInteger = Int

is an associated type declaration with a type constraint (`: SignedInteger`) and a default value (`= Int`),

If a type `T` adopts the protocol and does not define `T.IndexDistance` otherwise then `T.IndexDistance` becomes a type alias for `Int`. This is the case for many of the standard collection types (such as `Array` or `String`), but not for all. For example

public struct AnyCollection<Element> : Collection

from the Swift standard library defines

    public typealias IndexDistance = IntMax

which you can verify with

let ac = AnyCollection([1, 2, 3])
let cnt = ac.count
print(type(of: cnt)) // Int64

You can also define your own collection type with a non-`Int` index distance if you like:

struct MyCollection : Collection {

    typealias IndexDistance = Int16
    var startIndex: Int { return  0 }
    var endIndex: Int { return  3 }

    subscript(position: Int) -> String {
        return "\(position)"
    }

    func index(after i: Int) -> Int {
        return i + 1
    }
}

Therefore, if you extend the concrete type `Array` then `count` is an `Int`:

extension Array {
    func whatever() {
        let cnt = count // type is `Int`
    }
}

But in a protocol extension method

extension Collection {
    func whatever() {
        let cnt = count // some `SignedInteger`
    }
}

everything you know is that the type of `cnt` is some type adopting the `SignedInteger` protocol, but that need not be `Int`. One can still work with the count, of course. Actually the compiler error in

    for index in 0...count { //  binary operator '...' cannot be applied to operands of type 'Int' and 'Self.IndexDistance'

is misleading. The integer literal `0` could be inferred as a `Collection.IndexDistance` from the context (because `SignedInteger` conforms to `ExpressibleByIntegerLiteral`). But a range of `SignedInteger` is not a `Sequence`, and that's why it fails to compile.

So this would work, for example:

extension Collection {
    func whatever() {
        for i in stride(from: 0, to: count, by: 1) {
            // ...
        }
    }
}

As of Swift 4.1, `IndexDistance` is no longer used, and the distance between collection indices is now always expressed as an `Int`, see

- SE-0191 Eliminate IndexDistance from Collection

In particular the return type of `count` is `Int`. There is a type alias

typealias IndexDistance = Int

to make older code compile, but that is remarked deprecated and will be removed in a future version of Swift.

Problem

When I'm extending `Collection` the type of `count` is `IndexDistance`. When I'm extending `Array` type the `count` is of type `Int` Why is there such a distinction? Is this a recent change or it's always been like this? I've read this answer but couldn't pick up much. The only thing I deemed related, but didn't understand was: Another advantage is that this[IndexDistance] also works correctly with array slices (where the index of the first element is not necessarily zero Not sure what that means. The reason I'm asking is that why does the code throw an error on Collection but doesn't do such on Array...even though both `count`s are ultimately an `Int`. ``` extension Collection where Element: Comparable{ func whatever(){ for index in 0...count{ // binary operator '...' cannot be applied to operands of type 'Int' and 'Self.IndexDistance' } } } extension Array where Element: Comparable{ func whatever(){ for index in 0...count{ // NO ERROR } } } ``` EDIT: Based on comments made by Martin and others, I've added an extra question. Likely this is the root cause of my question... Does it mean that within `Collection` type the `IndexDistance` isn't defined to `Int`. Basically in general at the 'Protocol' level associatedTypes aren't defined...It's waiting for a concrete type to do that? Is that right? That being said is there any meaningful use case for accessing `count` at the 'Protocol' level? I mean you can't compare it against any `Int` so it seems pretty useless.

Original source

Related problems