Testing nested arrays for equivalence

Problem:


Testing arrays (with the same type of Equatable element type) for equivalence works as expected:

// This works:
let a = [1, 2, 3]
let b = [1, 2, 3]
print(a == b) // true


While the same is not true for nested arrays (eg arrays of arrays with the same type of Equatable element type):

// This will not compile:
let a = [[1, 2, 3], [10, 20, 30]]
let b = [[1, 2, 3], [10, 20, 30]]
print(a == b) // Compile Error: Binary operator '==' cannot be applied to two [Array<Int>] operands.


EDIT: Actually, as it turned out below, this will already work but only if we import Foundation / Cocoa ... But it's unclear how/why. Can someone explain?

But for now, let's assume we are in a pure Swift context, so no imports.


You could of course do eg this:

print(a.elementsEqual(b) { $0.0.elementsEqual($0.1) }) // true

Or write a == operator for arrays of arrays with equatable elements.

But these solutions will only work for the special case of exactly one level of nestedness.



My current solution:


Below is the least ugly general solution I can come up with (in Swift 2).


It would be much better if we could make arrays with equatable element types conform to Equatable, but AFAICS that is not possible. Here's a thread about that:

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


The main disadvantage of the solution below is that we have to state explicitly that an array is equatable (through the eq computed property which returns a custom EquatableSequence) and then if we want to get back the array we will have to do that explicitly as well (through the ar computed property).


// ---- Solution ----

extension SequenceType where Generator.Element : Equatable {
    var eq: EquatableSequence<Self> { return EquatableSequence(self) }
    var ar: Array<Generator.Element> { return Array<Generator.Element>(self) }
}

protocol EquatableSequenceType: SequenceType, Equatable { }

struct EquatableSequence<S : SequenceType where S.Generator.Element : Equatable>: EquatableSequenceType {
    let base: S
    init(_ base: S) { self.base = base }
    func generate() -> S.Generator { return base.generate() }
    func underestimateCount() -> Int { return base.underestimateCount() }
}

func ==<T: EquatableSequenceType where T.Generator.Element: Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs.elementsEqual(rhs)
}

// ---- Demo ----

// Array of arrays turned into equatable sequences:
let x = [[1, 2].eq, [1, 2, 3].eq]
let y = [[1, 3].eq, [1, 2, 3].eq]
print(x == y) // true

// Arrays turned into equatable sequences:
let a = [1, 2, 3].eq
let b = [1, 2, 3].eq
let c = [3, 2, 1].eq

// Nested equatable sequences:
let aa = [a, a].eq
let ab = [a, b].eq
let ac = [a, c].eq

// Nested nested equatable sequences:
let aaaa = [aa, aa].eq
let aaab = [aa, ab].eq
let aaac = [aa, ac].eq

print(a == b) // true
print(a == c) // false
print(aa == ab) // true
print(aa == ac) // false
print(aaaa == aaab) // true
print(aaaa == aaac) // false

print(aaaa.ar[0].ar[0].ar[0])  // Solution is not very nice when you want to get back to a
                              // nested array again, could of course write an EquatableArray
                              // analogous to EquatableSequence but that would mean a lot of
                              // forwarding methods.


Has anyone arrived at a less hackish general solution for testing nested arrays for equivalence?

Interestingly, checking equality of nested arrays seems to work without any additional hacking for certain types (Int, Float, Double, UInt) but not others, in a playground in Xcode 7.0 beta (7A121l):


typealias TestType = Double

// Arrays turned into equatable sequences:
let a: [TestType] = [1, 2, 3]
var b: [TestType] = []; b.append(1); b.append(2); b.append(3)
let c: [TestType] = [3, 2, 1]
print(a == b) // true
print(a == c) // false

// Nested equatable sequences:
let aa = [a, a]
let ab = [a, b]
let ac = [a, c]
print(aa == ab) // true
print(aa == ac) // false

// Nested nested equatable sequences:
let aaaa = [aa, aa]
let aaab = [aa, ab]
let aaac = [aa, ac]
print(aaaa == aaab) // true
print(aaaa == aaac) // false

let axa = [aaaa, aaaa]
let axb = [aaaa, aaab]
let axc = [aaaa, aaac]
print(axa == axb) // true
print(axa == axc) // false

let axxa = [axa, axa]
let axxb = [axa, axb]
let axxc = [axa, axc]
print(axxa == axxb) // true
print(axxa == axxc) // false

let info = axxa.dynamicType // Array<Array<Array<Array<Array<Double>>>>>.Type


For the types that work, using option-click on the == operator shows that it is the standard [T: Equatable] version, even for nested arrays...

func ==<T : Equatable>(lhs: [T], rhs: [T]) -> Bool


Which continues to work in extreme cases for those particular types, and also offers an interesting oddity:

let xa = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[axxa, axxa]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
let xb = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[axxa, axxb]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
let xc = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[axxa, axxc]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
print(xa == xb) // true
print(xa == xc) // false

let xxa = [xa, xa]
let xxb = [xb, xb]
let xxc = [xa, xc]
print(xxa == xxb) // true
print(xxa == xxc) // false


// ???...

print(a == xxa)
print(c == xxc)

if (TestType.self != Int.self)
{
    let other: [Int] = [3]
    print(other == xxa)
}
else
{
    let other: [Double] = [3.0]
    print(other == xxa)
}


Somehow, thecomparisons between Array<TestType> and WayTooManyArrays<TestType> are also allegedly using the standard [T: Equatable] version of the == operator, even though the two parameters are entirely different types. And it will even compare Array<SomeOtherType> with WayTooManyArrays<TestType>.

But back to the somewhat flippant but still potentially useful answer I was originally planning to post, before I got distracted while testing it in the playground...


The following test works surprisingly well for value types, and should be exact for most (if not all) of the Swift standard numerical types:

func arePotentiallyEqualNestedArrays<T>(a: [[T]], _ b: [[T]]) -> Bool
{
    return (a.description == b.description)
}


The result will be exact as long as the string returned by the type nested within the arrays is identical for equal values and different for unequal values.

If your values/protocol have a way of outputting themselves as a String that exactly represents them, returning that string as the description would make this exact for those types.

So this is only in the playground?


EDIT: No, I checked, it depends on if you import Cocoa (or Foundation), so it's not something particular to the playground.

This is really strange, I can't understand why importing Cocoa / Foundation will make it work, do they add some extension to array? (Because they are not NSArrays, and the == operator is the same as the one that only works with non-nested arrays without importing Foundation.) Perhaps it's the new automatic converting of Swift Arrays into NSArrays in Swift 2 ...


I guess it's not exectly the same peculiarity as this:

// import Foundation
print([[1, 2, 3], [1, 2, 3]] == [[1, 2, 3], [1, 2, 3]]) // <-- Will be true or false depending on if we import Foundation or not(!)

on which you can get details in this thread:

https://forums.developer.apple.com/message/18394

but maybe related.

I think I'm looking for more of a protocol-oriented-programming-best-practices type of answer : ) Ie, how are we are supposed to do this the right way, as in "what would the standard library designers do?".


For example, it's pretty clear that it will not work for eg Double (or Float):

let a = 1.0 / 3.0
let b = a
let c = Double(a.description)!
print(a == b) // true
print(a == c) // false : (


That is, the "0.333333333333333" of description property is not a lossless representation of the double precision floating point type approximation of 1/3. And it's the same for eg Float(1.0/3.0). Not to mention that I prefer not to compare strings instead of the actual values, and I only want it to work when the "leaves" of the two nested arrays are Equatable, and the two arrays are of the same type. I want compile time errors otherwise.

This gives us some hints:

infix operator |==| {}
func |==|<T : Equatable>(lhs: [T], rhs: [T]) -> Bool {
    print("lhs: \(lhs.dynamicType)")
    print("rhs: \(rhs.dynamicType)")
    return lhs == rhs
}
print([[1, 2, 3], [1, 2, 3]] |==| [[1, 2, 3], [1, 2, 3]])
// With import Foundation
//->lhs: Swift.Array<NSArray>
//->rhs: Swift.Array<NSArray>

Ah, yes, and NSArray conforms to Equatable no matter what the type of elements, so eg:

// import Foundation // Uncomment to enable typechecking and compile time error! :P
print([1, 2, 3] == ["1", "2", "3"])


There certainly are quite a lot of issues that have to do with the automatic conversion and the bridging of NSArray and Array ...


It's also ... interesting to note that your example code won't even compile if we comment out import Foundation, but this time it's print that will or will not take Bool as an argument:

// import Foundation

infix operator |==| {}
func |==|<T : Equatable>(lhs: [T], rhs: [T]) -> Bool {
    print("lhs: \(lhs.dynamicType)")
    print("rhs: \(rhs.dynamicType)")
    return lhs == rhs
}
print([[1, 2, 3], [1, 2, 3]] |==| [[1, 2, 3], [1, 2, 3]]) // Compile Error: Cannot find an overload of 'print' that accepts an argument list of type '(Bool)'


It's like a strange new world waiting for us to be explored by commenting and uncommenting import Foundation.

I wonder when all this will be fixed, this is the sort of thing that makes you think you can't trust anything.

The standard library developers would probably decide how many dimensions it is reasonable to support, and then either write the code necessary to handle those first few n-dimension cases, or write a new value Type that stores the data using a different model?


At what point would an entirely different kind of model start to make more sense than adding more and more dimensions to the nested array? What kinds of use cases are you envisioning?

I don't have a particular use case. I'm trying to get a deeper understanding of the language, learn how to do proper protocol oriented programming, etc and I'm trying to use the std lib for inspiration.


While doing this I came across all these "issues" with arrays, and I just thought it was odd that arrays with equatable element types doesn't conform to Equatable themselves (because if they did/could, this particular problem would be solved, elegantly and for all levels of nestedness, typechecking intact), and I thought of it as a nice exercise to try and implement that.


The current std lib situation/implementation for this is just very strange/surprising: If you don't import Foundation, you can't check if two nested arrays (of the same type, and with equatable "leaves" of course) are equal, because the array type itself doesn't conform to Equatable. And conversively, if you do import Foundation you will be able to check if any two nested arrays (of any types) are equal, so no type checking anymore, because of NSArray bridging, and NSArray conform to Equatable no matter what type of elements it has ...


And it certainly is possible to write an array type (and/or SequenceType) that will conform to Equatable iff its element type is Equatable, thus it will also work with nested such types.


My solution in the orignal post above does just that, therefore I guess this could have been done for the Array type in the std lib as well, but for some reason it isn't designed like that (which I think would be the obvious thing to do). So I wonder why they didn't do that, perhaps because of the need/will to bridge Array and NSArray.


As it is now, It's like the implementation of testing arrays for equivalence is something that has just been tucked on ignoring the rest of the type hierarchy in the std lib, so that it only kind of half-works and brings all this odd behaviour. Instead of using the Equatable protocol they've decided to go with some ad hoc solution which has two different behaviours depending on if we import Foundation or not, and each of the two behaviours is strange in its own unique way.

The obvious "correct" solution is to make Array conform to Equatable when its T is Equatable:


extension Array: Equatable where T: Equatable {}


But, as you noticed, this isn't currently possible. Array isn't the only type like this, either—Optional and various other collections need it as well. Searching the standard library pseudoheader for "func !=" will give you an idea of the types that ought to be Equatable but aren't yet. The whole standard library is crying out for it.


I don't think there's any theoretical reason it couldn't be done, either, so I think we'll get it eventually; Swift just doesn't support it yet. Presumably they need to lay some more groundwork first. We could, I suppose, get it later in the beta cycle, or in Swift 2.1 or Swift 3.


Until then, your best bet is probably just to keep writing appropriately-dimensioned == and != operators (don't forget to do both). It's not great, but as the standard library's "func ==<T: Equatable>(lhs: [T], rhs: [T]) -> Bool" operator shows, it's the best that today's Swift has to offer.

Just to clarify, I wasn't trying to criticize what you are doing. I enjoy learning by trying to solve problems too, and that's mostly why I'm on these forums.


But I think that "Why is this so hard?" is a question that doesn't always get asked, and it often would be helpful to know the answer:

This is one of the main reasons why I am excited about Swfit becoming open source, since the Swift team will be better able to discuss future plans and whether or not a particular feature is intended to be added in the future. I think that conditional protocol compliance for generics is probably something that they would like to add, and it just hasn't happened yet.


While it is true that some useful things are going to be difficult to implement in a particular language, often there is a different way to accomplish the same goal or a different model that avoids the issue. It seems like people sometimes spend a lot of time fighting to make their code work a particular way, only to discover later that some minor change to the original design breaks everything they have done or would have made it unneccessary in the first place.

Agreed. That's exactly what my intentions are, learning how to not fight the laguage. I just thought that in this particular case, the thing that was proving to be hard was also the thing that seemed to be the most appropriate/non-fighting thing to do, which probably means that future arrays will be equatable iff their element type is equatable (and similar stuff will be realized for related types and protocols). But as Swift and the std lib is evolving so quickly it can be hard to know if something is hard because you are fighting or "predicting" the language so to speak.

This is just the tip of the iceberg. I agree that the right means to express that `Array<T>` is `Equatable` if `T` is `Equatable` is a conditional struct extension of `Array`:


extension Array: Equatable where T: Equatable {}


But even if we assume that Swift would allow such conditional extensions, it's not clear to me how you would be able to implement the corresponding `==` operator. Right now, `Equatable` is defined like this:


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


But the implementation of the `==` operator is done as a top-level function and will have to look like this so that you're able to use `==` on T values in the function body:


func ==<T: Equatable>(lhs: Array<T>, rhs: Array<T>) -> Bool {
  ...
}


It's not clear to me how such a generic function would ever match the non-generic function expressed by protocol `Equatable`.

ObjectHub wrote:

It's not clear to me how such a generic function would ever match the non-generic function expressed by protocol `Equatable`.


The == operator function that Equatable requires is not "non-generic", as it is expressed in terms of Self (ie whatever type that happens to conform to the protocol).

Operator functions required by protocols are defined globally, and generics is not a problem. Here is a similar example, working as expected:

infix operator £ { associativity none precedence 130 }

func £<T: Poundable>(lhs: T, rhs: T) -> T {
    return lhs.poundedWith(rhs)
}
protocol Poundable {
    func £(lhs: Self, rhs: Self) -> Self
    func poundedWith(other: Self) -> Self
}
extension String : Poundable {
    func poundedWith(other: String) -> String {
        return "\(self)£\(other)"
    }
}
extension Double : Poundable {
    func poundedWith(other: Double) -> Double {
        return self * 100_000_000.0 + other
    }
}
print("foo" £ "bar") // foo£bar
print(12.34 £ 56.78) // 1234000056.78


Note that the global generic £ operator function is providing the implementation of the required £ operator function for both the String and Double extensions.

Thanks for clarifying, Jens. The "mechanism" that exists today should indeed work for arrays as well and, in theory, should extend to conditional protocol conformance.


== Matthias

Testing nested arrays for equivalence
 
 
Q