Duncan C Duncan C - 29 days ago 12
Swift Question

Is there a zip function to create tuples with more than 2 elements?

I just discovered the Swift

zip
function recently. It seems quite useful.

It takes 2 input arrays and creates an array of tuples out of pairs of values from each array.

Is there a variant of zip that takes an arbitrary number of arrays and outputs tuples with that same number of elements? It seems like there should be a way to do this.

Answer

No, zip for an arbitrary number of sequences isn't currently possible due to Swift's lack of variadic generics. This is discussed in the Generics Manifesto.

In the meanwhile, you can implement your own based off the existing (open source) implementation of Zip2Sequence. Here's a working example of Zip3Sequence:

public func zip<Sequence1 : Sequence, Sequence2 : Sequence, Sequence3 : Sequence>(
    _ sequence1: Sequence1, _ sequence2: Sequence2, _ sequence3: Sequence3
    ) -> Zip3Sequence<Sequence1, Sequence2, Sequence3> {
    return Zip3Sequence(_sequence1: sequence1, _sequence2: sequence2, _sequence3: sequence3)
}

public struct Zip3Iterator<
    Iterator1 : IteratorProtocol, Iterator2 : IteratorProtocol, Iterator3 : IteratorProtocol
> : IteratorProtocol {
    /// The type of element returned by `next()`.
    public typealias Element = (Iterator1.Element, Iterator2.Element, Iterator3.Element)

    /// Creates an instance around a pair of underlying iterators.
    internal init(_ iterator1: Iterator1, _ iterator2: Iterator2, _ iterator3: Iterator3) {
        (_baseStream1, _baseStream2, _baseStream3) = (iterator1, iterator2, iterator3)
    }

    /// Advances to the next element and returns it, or `nil` if no next element
    /// exists.
    ///
    /// Once `nil` has been returned, all subsequent calls return `nil`.
    public mutating func next() -> Element? {
        // The next() function needs to track if it has reached the end.  If we
        // didn't, and the first sequence is longer than the second, then when we
        // have already exhausted the second sequence, on every subsequent call to
        // next() we would consume and discard one additional element from the
        // first sequence, even though next() had already returned nil.

        if _reachedEnd {
            return nil
        }

        guard let element1 = _baseStream1.next(),
            let element2 = _baseStream2.next(),
            let element3 = _baseStream3.next() else {
                _reachedEnd = true
                return nil
        }

        return (element1, element2, element3)
    }

    internal var _baseStream1: Iterator1
    internal var _baseStream2: Iterator2
    internal var _baseStream3: Iterator3
    internal var _reachedEnd: Bool = false
}

public struct Zip3Sequence<Sequence1 : Sequence, Sequence2 : Sequence, Sequence3 : Sequence>
: Sequence {

    public typealias Stream1 = Sequence1.Iterator
    public typealias Stream2 = Sequence2.Iterator
    public typealias Stream3 = Sequence3.Iterator

    /// A type whose instances can produce the elements of this
    /// sequence, in order.
    public typealias Iterator = Zip3Iterator<Stream1, Stream2, Stream3>

    @available(*, unavailable, renamed: "Iterator")
    public typealias Generator = Iterator

    /// Creates an instance that makes pairs of elements from `sequence1` and
    /// `sequence2`.
    public // @testable
    init(_sequence1 sequence1: Sequence1, _sequence2 sequence2: Sequence2, _sequence3 sequence3: Sequence3) {
        (_sequence1, _sequence2, _sequence3) = (sequence1, sequence2, sequence3)
    }

    /// Returns an iterator over the elements of this sequence.
    public func makeIterator() -> Iterator {
        return Iterator(
            _sequence1.makeIterator(),
            _sequence2.makeIterator(),
            _sequence3.makeIterator())
    }

    internal let _sequence1: Sequence1
    internal let _sequence2: Sequence2
    internal let _sequence3: Sequence3
}

In action:

let integers = [1, 2, 3, 4, 5]
let strings = ["a", "b", "c", "d", "e"]
let doubles = [1.0, 2.0, 3.0, 4.0, 5.0]

for (integer, string, double) in zip(integers, strings, doubles) {
    print("\(integer) \(string) \(double)")
}

Prints:

1 a 1.0

2 b 2.0

3 c 3.0

4 d 4.0

5 e 5.0