How to elegantly compare tuples in Swift?
Asked Answered
K

7

33

I do have 2 different tuples of type (Double, Double):

let tuple1: (Double, Double) = (1, 2)
let tuple2: (Double, Double) = (3, 4)

I want to compare their values using a simple if statement. Something like:

if (tuple1 == tuple2) {
    // Do stuff
}

This throws the following error:

Could not find an overload for '==' that accepts the supplied arguments

My current solution is a function like this:

func compareTuples <T: Equatable> (tuple1: (T, T), tuple2: (T, T)) -> Bool {
    return (tuple1.0 == tuple2.0) && (tuple1.1 == tuple2.1)
}

I already tried to write an extension but can't make it work for tuples. Do you have a more elegant solution to this problem?

Kinnon answered 30/6, 2014 at 9:59 Comment(1)
As of Swift 2.2.1, the standard lib comes with this definition: public func ==<A : Equatable, B : Equatable>(lhs: (A, B), rhs: (A, B)) -> BoolAdjacent
R
31

Update

As Martin R states in the comments, tuples with up to six components can now be compared with ==. Tuples with different component counts or different component types are considered to be different types so these cannot be compared, but the code for the simple case I described below is now obsolete.


Try this:

func == <T:Equatable> (tuple1:(T,T),tuple2:(T,T)) -> Bool
{
   return (tuple1.0 == tuple2.0) && (tuple1.1 == tuple2.1)
}

It's exactly the same as yours, but I called it ==. Then things like:

(1, 1) == (1, 1)

are true and

(1, 1) == (1, 2)

are false

Remex answered 30/6, 2014 at 10:25 Comment(7)
No, you're creating an overload for ==, you are not overriding anything. I have to admit I'm surprised it doesn't already exist but if Apple get around to putting one in, I can't see how the implementation would be different from yours.Remex
Most likely this isn't implemented on the standard library because there is no generic way to define this operator for tuples of N elements. AFAIK, this would only be possible with an equivalent of C++'s variadic templates.Jeffereyjefferies
Tuple could have any number of elements. This answer works for two-element tuple only. I would not consider this one "elegant".Rucksack
@Rucksack if you want a type that can hold an unspecified number of objects of the same class, use an array.Remex
This is nice, but it doesn't allow the tuple to conform to Equatable, (at least in swift 1.1), which means it can't be used in generics or in XCTAssertEqualBusman
As of Swift 2.2 (Xcode 7.3.1), tuples up to arity 6 can be compared with ==, see github.com/apple/swift-evolution/blob/master/proposals/….Fi
Indeed, (1,2,3,4,5,6)==(1,2,3,4,5,6) returns Bool = true, but LHS & RHS must have equal length/size, otherwise it is an error, not a false.Calli
S
12

I agree that this behavior is not expected, since tuples can be compared in diverse languages such as Python and Haskell, but according to the official documentation:

NOTE

Tuples are useful for temporary groups of related values. They are not suited to the creation of complex data structures. If your data structure is likely to persist beyond a temporary scope, model it as a class or structure, rather than as a tuple. For more information, see Classes and Structures.

So, this might be the Swift equivalent of "you're holding the phone wrong," but by current guidelines, the idiomatic way to do this is to define a Class or Struct (or even an Enum) with a Comparable implementation to provide == and related operators.

Satiny answered 30/6, 2014 at 16:50 Comment(5)
Coming from Python I must say, that this takes a lot of the elegance from tuples… :(Kinnon
@ThomasJohannesmeyer Well, what can you do? In the presence of strong typing, tuples are not Equatable in general. If your type system does not have features that help you around that, there's little you can do. In other words, what you call "elegance" in Python comes at the price of static type checking; you can always write (a,b) == (c,d) but it won't always execute. Personally, I'd rather only be allowed to write things that I know will execute later.Apicella
@Apicella I don't understand why this is a problem. Swift knows the types of my tuples. It should know how to compare them at compile time.Tannen
@Tannen Conceptually, yes. At the time of my writing above comment, Swift didn't even know how to deal with arrays of Equatables. This has been dealt with now. However, afaik tuple are special constructs and not normal types, so any automatism would have to be in the compiler (as opposed to extensions). That said, I think auto-generating == for suitable tuples should be feasible. Maybe we'll see an SE in the future!Apicella
Seems that the tuples in Python are like immutable arrays, but they're more like anonymous structs in Swift.Tannen
W
7

Swift 4 supports tuple comparison. You won't get the error anymore.

This code runs perfectly

let tuple1 : (Double, Double) = (1,2)
let tuple2 : (Double, Double) = (3,4)

if (tuple1 == tuple2) {
    print("equal")
}
else {
    print("unequal")
}

Here, unequal is printed in the console of the playground.

One limitation in tuple comparison as mentioned in the apple doc is -

The Swift standard library includes tuple comparison operators for tuples with fewer than seven elements. To compare tuples with seven or more elements, you must implement the comparison operators yourself.

Wandie answered 12/11, 2018 at 16:54 Comment(3)
One confusing thing is that as of now (Swift 4), if you try to use XCTAssertEqual the compiler will tell you: Global function 'XCTAssertEqual(_:_:_:file:line:)' requires that '(String, String)' conform to 'Equatable'Endocardium
(Xcode 11.1/Swift 5) I also get the XCTAssertEqual error, and have to instead use XCTAssert with the == operator and then it works. My feeling is that this behavior is a bug.Flavio
Indeed, (1,2,3,4,5,6)==(1,2,3,4,5,6) returns Bool = true, but LHS & RHS must have equal length/size, otherwise it is an error, not a false.Calli
P
3

In swift there are tuples as we know. Tuples can be compared with each other using standard C operators. Tuples are compared with each other from LEFT to RIGHT

if (1,"death") < (3,"life") {   
     print("Life is better than death") // this is true
}

Swift only compares the integer values 1 and 3. That's it, Swift does not compare the strings of death and life. It will only compare them when the first elements of the tuple are the same.

if (99,"life") < (99,"death") {   
     print("Life is better than death") // this is false
}

In the above case swift does not compare the integer values of 99, instead it will compare life and death. Additionally: Swift can only compare tuples which have max 6 elements. More than 6 elements you have to compare it by yourself.

Possum answered 16/7, 2017 at 8:11 Comment(0)
R
2

Similar to the answer of @JeremyP, but more generic:

func ==<T1: Equatable, T2: Equatable>(lhs: (T1, T2), rhs: (T1, T2)) -> Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1
}

func ==<T1: Equatable, T2: Equatable, T3: Equatable>(lhs: (T1, T2, T3), rhs: (T1, T2, T3)) -> Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1 && lhs.2 == rhs.2
}

func ==<T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable>(lhs: (T1, T2, T3, T4), rhs: (T1, T2, T3, T4)) -> Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1 && lhs.2 == rhs.2 && lhs.3 == rhs.3
}
Refreshing answered 27/8, 2019 at 10:51 Comment(1)
How would you even get that to build? Could you give an example how you actually use this?Bowerman
P
1

The following approach compares tuple of any number of members of any size, provided they do not contain collection types like Array and Dictionary.

import Darwin // or Foundation
/// here we go
func memeq<T>(var lhs: T, var rhs: T) -> Bool {
    return withUnsafePointers(&lhs, &rhs) {
        memcmp($0, $1, UInt(sizeof(T)))
    } == 0
}

let l = (false, 42,  log(exp(1.0)))
let r = (!true, 6*7, exp(log(1.0)))
println(memeq(l, r))   // expectedly true
let l2 = (0, [0])
let r2 = (0, [0])
println(memeq(l2, r2)) // unfortunately false

Note types are already checked via generics. If they differ, it does not even compile thanks to type checking.

Preceptive answered 18/7, 2014 at 0:29 Comment(4)
Your code relies on a lot of assumptions (values of padding bytes, types are not classes, types did not define an equality operator, etc.) which cannot generally be confirmed. So, this code is not reliable.Holmberg
Thank you! This answer saved my day! I literally broke my mind trying to compare two arrays from C header defined as char[52]. Spent several hours fighting the compiler!Kristakristal
how why and wow about log(exp(1.0)) == exp(log(1.0)). I sure would love to read a blog post explaining this. Also, why l2 != r2 ? different memory locations ?Lowering
This method compares the byte representation in memory of the two arguments. It short of works for tuples and structs as long as they contain scalar values, but if they contain any pointers (to objects, arrays, dictionaries, etc) it no longer works.Kneecap
G
0

This solution is based on my previous answer How to compare "Any" value types but includes tuple comparison for any number of items by any type:

fileprivate extension Equatable {
  func isEqual(to: Any) -> Bool {
    self == to as? Self
  }
}

func ==<T>(lhs: T?, rhs: T?) -> Bool where T: Any {
  guard let lhs, let rhs else {
    return lhs == nil && rhs == nil
  }
  
  // Equatable
  if let isEqual = (lhs as? any Equatable)?.isEqual {
    return isEqual(rhs)
  }
  // [Any]
  else if let lhs = lhs as? [Any], let rhs = rhs as? [Any], lhs.count == rhs.count {
    return lhs.elementsEqual(rhs, by: ==)
  }
  // [AnyHashable: Any]
  else if let lhs = lhs as? [AnyHashable: Any], let rhs = rhs as? [AnyHashable: Any], lhs.count == rhs.count {
    return lhs.allSatisfy { $1 == rhs[$0] }
  }
  // (Any...)
  else {
    let ml = Mirror(reflecting: lhs)
    let mr = Mirror(reflecting: rhs)
    guard ml.children.count == mr.children.count else {
      return false
    }
    return zip(ml.children, mr.children).allSatisfy { $0.value == $1.value }
  }
}

For instance:

let t1 = (1, "2", 3.0, ["4" : 5], 6..<7, 8, [9, 10])
let t2 = (1, "2", 3.0, ["4" : 5], 6..<7, 8, [9, 10])
t1 == t2
Gorgonzola answered 25/3 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.