Why can't I make Array<T: Equatable> conform to Equatable?

Any Array with an element type that conforms to Equatable can be checked for equality like eg:

let a = ["one", "two"]
let b = ["one", "foo"]
print(a == b) // false


This is not because Arrays in general, or Arrays with Equatable element types in particular, conform to Equatable (neither does). But the standard library has this:

/// Returns true if these arrays contain the same elements.
func ==<T : Equatable>(lhs: [T], rhs: [T]) -> Bool


I can understand why all possible Array<T> types (for any T) can't conform to Equatable (because it simply wouldn't be possible to check the element-pairs of any two such arrays for equality (since their element type wouldn't have to be Equatable)).


But what about all Array<T: Equatable> types? I can't see any good reason for why those shouldn't / couldn't conform to Equatable. Please let me know if you can.


Details:


Here's what the Equatable protocol requires:

protocol Equatable {
    func ==(lhs: Self, rhs: Self) -> Bool
}


That requirement is, as far as i can see, already satisfied by any and all Array<T: Equatable> types (see the above == function from the standard library), the only thing missing is the declaration of that conformance, something like

extension Array where T: Equatable: Equatable {}

or

extension Array: Equatable where T: Equatable {}

none of which compiles (which seems reasonable looking at the syntax alone, but I can't come up with any other way to express that).


For reference, here's an alternative implementation of the above ==:

public func ==<T: CollectionType where T.Generator.Element: Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs.count == rhs.count && (zip(lhs, rhs).contains { $0.0 != $0.1 }) == false
}


So, I must be missing something here, probably one of these:

1. An existing way to declare that all Array<T: Equatable> types conform to Equatables (but if so, why hasn't that been done in the std lib?).

2. A possible future way to declare that all Array<T: Equatable> types conform to Equatable.

3. A good reason for why this isn't and never will be possible.


Which one is it?

Replies

I think this is just a current limitation of the type system, since there is a rather specific error if you try to do this ("Extension of type 'TypeName' with constraints cannot have an inheritance clause"). It's definitely a little disappointing. I filed a Radar (#21507148), and I'm sure others have done so as well.

Thanks, that makes sense (unless somebody has a good explanation for why extensions with constraints cannot have an inheritance/conformance clause, neither now nor in the future) .

For completeness I'll note that there's a somewhat related and rather peculiar issue that can be demonstrated like this:

// import Foundation
let hmm = [[1, 2, 3]] == [[1, 2, 3]]
print(hmm) // Will print false or true depending on if import Foundation is uncommented or not.


Also, note that the following won't compile:

let a = [[1, 2, 3]]
let b = [[1, 2, 3]]
let hmm = a == b // Error: Binary operator '==' cannot be applied to two [Array<Int>] operands.


While the following will compile and be equivalent to the first example:

// import Foundation
let a = [1, 2, 3]
let b = [1, 2, 3]
let hmm = [a] == [b] // Error: Binary operator '==' cannot be applied to two [Array<Int>] operands.
print(hmm)


I understand perfectly well how and why this happens, but it might be considered a somewhat surprising behaviour, to say the least.


Here's the thread that brought this to my attention:

https://forums.developer.apple.com/thread/7615


I'm looking forward to hearing some short words on this from the Swift team.

So I tried this with what I think the syntax would look like:


extension Array<T: Equatable> : Equatable {


And Swift 1.2 compiler says "Extension of generic type 'Array' cannot add requirements"


Whereas Swift 2 give a much more oblique message: "Expected > to complete generic argument list"


So ... I am surmising that extensions of all generics cannot add requirements.

It seems like it would push the whole conformance-checking system further into runtime/dynamic territory. Protocol conformance is now dynamically checkable, but it can be relatively efficient because all objects of type X either will or will not be Y-compliant.


Consider:


Code region A:

protocol Tweakable {}
let a = [Int]()
...
if a is Tweakable {
    ...
}

Code region B:

extension Aray where T: Hashable: Tweakable {}


The existence of region B makes [Int] tweakable and so should change A's behavior, even if they are in separate modules. To preserve correctness, the compiled code for A would have to run through some kind of runtime list of possible conformances and their type constraints in order to answer the "is" question.


As it is now, you can in fact do


extension Array: Tweakable {}


But that information probably only has to be recorded in one place and is true for all Arrays, so the dynamic check is relatively efficient.


I suppose the compiler or linker could generate some kind of cartesian product of all types mentioned in all modules vs. all known type constraints and try to precompute the results, but this doesn't seem very scalable.


Constrained extensions as they currently exist can be implemented either at compile time or through dispatch tables. Much like generic functions with multiple signatures.

Protocols with self or associated type constraints (like Equatable) are not dynamic at all. They are fully compile-time determined based on the static type. The downside is that they can't be used as a type, only a type constraint.


Allowing an extension to have both constraints and an inheritance clause shouldn't change this. It would still be a fully compile-time construct in the same way. I assume the only reason it hasn't been done is that it's an extra layer of complexity that someone needs to code into the compiler.

Just to clarify: I only wrote Array<T: Equatable> as a short/sloppy way of saying Array<T> where T (the element type, Element) conforms to Equatable.


I think the syntax would probably be closer to one of the two alternatives I gave in the original post. The reasons for that can perhaps be seen by looking at this:


// Here's is how it's currently possible (in Swift 2) to extend Array<T> where T (the element type, Element) conforms to Equatable.

extension Array where T: Equatable {
    func countElementsEqualTo(value: Element) -> Int {
        // This method is only available for arrays where the element type (T / Element) conforms to Equatable.
        return self.reduce(0) { $0.1 == value ? $0.0 + 1 : $0.0  }
    }
}

let a1: [Int] = [2, 8, 2, 2, 7, 3, 2, 6]
let a2: [Any] = ["one", 2, 3.0, UInt8(4)]

print(a1.countElementsEqualTo(2)) // 4
// print(a2.countElementsEqualTo(2)) // Error (as expected, since Any doesn't conform to Equatable)


So, since the above is how the syntax is for extending Array<T>'s with constraints on T (its Element type), it seems natural to assume that the syntax to declare protocol conformance for Array<T> with constraints on T would be something like the two alternatives I tried in my first post. And as austinz said above: The "rather specific error" you get when using one of those alternatives, namely this:

extension Array: Equatable where T: Equatable { } // Error: Extensoin of type Array with constraints cannot have an inheritance clause.

might suggest that it is just a current limitation of the type system. But I'm still not sure whether this is actually the case, or if there is some good reason for why we will/should never be able to do this.


By the way, as a side note, if we try to call the method on an array type where it shouldn't be possible ([Any] in the case of a2 above), the error message we get is currently not very informative:

print(a2.countElementsEqualTo(2)) // Misleading error message 1: Cannot invoke 'countElementsEqualTo' with an arguement list of type '(int)'
let x: Any = 2
print(a2.countElementsEqualTo(x)) // Misleading error message 2: Cannot invoke 'countElementsEqualTo' with an arguement list of type '(Any)'

It would of course be clearer what the actual error was if the messages told us that the method isn't available for arrays whose element type does not conform to Equatable.

(It's also kind of misleading that Xcode (7 beta 2) will show this method as a possible auto completion not only of "a1." but also of "a2.".

I see your point.


Protocols with type requirements can't even be named in "is ***" clauses, so in broad outlines they really cast no runtime shadow other than as dispatch tables. By the time you get to running code, the type is always known to be something more specific.


Constraints on protocol extensions have to be expressed in terms of existing type requirements; otherwise, there is nothing to constrain. Ergo, protocols that are constraint-extended are necessarily of dynamically-unnameable protocol types even before they are extended, keeping the problem in the compiler's domain.


Similar (but not exactly identical) logic seems to apply to actual types and their generic specializations, and so on for the constrained extensions based on them.


The only leak that I see occurs in the type of case I outlined above: a constrained extension that adds conformance to a simple protocol. Just to repeat:


protocol Tweakable {}
extension Array where T: Hashable: Tweakable {}

Here, Tweakable has no associated types, so code is free to ask if "x is Tweakable" at run time. So in the original example, I continue to think that correctness-preserving interaction between code regions A and B would require dynamic support.


But you could probably get around this just by requiring that conformances added by constrained extensions be for protocols that themselves have associated type requirements.


extension Array where T: Equatable: Equatable    // ¡Si!
extension Array where T: Equatable: BooleanType  // ¡No!


Good luck explaining this restriction in thirty seconds, though. :-)