ctietze
ctietze

Reputation: 2932

Swift zip generator only iterates one side twice

I use zip on two arrays of tuples. When I iterate the result, the result tuple contains tuples from the left-hand side only.

Interestingly, Array(zip(...)) produces a collection as expected. I want to save a few cycles and memory and prefer not to generate a new array just for the sake of looping.

let expectation: [(String, UInt)] = [("bar", 0)]
let comparison: [(String, Int)] = [("foo", 0)]

func ==(lhs: [(String, UInt)], rhs: [(String, Int)]) -> Bool {
    if lhs.count != rhs.count {
        return false
    }

    for (l, r) in zip(lhs, rhs) {
        // Looking at `l` and `r` in lldb shows both are the same.
        if l.0 != r.0 || Int(l.1) != r.1 {
            return false
        }
    }

    return true
}

let equals = (expectation == comparison) // true?!?!

This was meant to be a convenience method to compare records of function calls in a test double to test data from the actual test cases. The double records (String, UInt), typing tuples in test cases produces (String, Int), so I thought: let's create an easy equality function! Changing UInt to Int doesn't change a thing.

How's that? Renders zip pretty useless to me (except when you can explain what's going on).

Upvotes: 2

Views: 157

Answers (3)

oisdk
oisdk

Reputation: 10091

Yeah, looks like a bug. Weirdly, though, if you replace the for loop with contains(), you don't need to deconstruct the tuple, and it works like you'd expect:

func ==(lhs: [(String, UInt)], rhs: [(String, Int)]) -> Bool {
  if lhs.count != rhs.count {
    return false
  }

  return !contains(zip(lhs, rhs)) {
    l, r in l.0 != r.0 || Int(l.1) != r.1
  }

}

Upvotes: 1

Airspeed Velocity
Airspeed Velocity

Reputation: 40965

Can’t decide whether this is a bug or just something I’m not understanding (suspect bug but need to play with it more).

However, here’s a workaround in the mean-time, which involves fully destructuring the data:

let expectation: [(String, UInt)] = [("bar", 0)]
let comparison: [(String, Int)] = [("foo", 1)]

func ==(lhs: [(String, UInt)], rhs: [(String, Int)]) -> Bool {
    if lhs.count != rhs.count {
        return false
    }

    for ((ls, li),(rs,ri)) in zip(lhs, rhs) {
        // Looking at `l` and `r` in lldb shows both are the same.
        if ls != rs || Int(li) != ri {
            return false
        }
    }

    return true
}

let equals = (expectation == comparison) // now returns false

In theory, this ought to be more easily written as:

equal(expectation, comparison) {
    $0.0 == $1.0 && Int($0.1) == $1.1
}

except that, infuriatingly, the equal function that takes a predicate still requires the elements of the two sequences to be the same! Radar 17590938.

A quick fix for this specific to arrays could look like:

func equal<T,U>(lhs: [T], rhs: [U], isEquivalent: (T,U)->Bool) -> Bool {
    if lhs.count != rhs.count { return false }
    return !contains(zip(lhs, rhs)) { !isEquivalent($0) }
}
// now the above use of equal will compile and return the correct result

p.s. you might want to add an overflow check for the UInt conversion

Upvotes: 3

Matteo Piombo
Matteo Piombo

Reputation: 6726

In support to @Airspeed Velocity, it seems a bug since you can define only the first tuple, than the second will work as expected :-/ Xcode 6.3.2 Swift 1.2

let expectation: [(String, UInt)] = [("bar", 0)]
let comparison: [(String, Int)] = [("foo", 1)]

func ==(lhs: [(String, UInt)], rhs: [(String, Int)]) -> Bool {
    if lhs.count != rhs.count {
        return false
    }

    for ((l0, l1), r) in zip(lhs, rhs) {
        // Explicitly detiled first tuple
        if l0 != r.0 || Int(l1) != r.1 {
            return false
        }
    }

    return true
}

let equals = (expectation == comparison) // false as expected 

Upvotes: 2

Related Questions