Add Various Zip-Related Types and Operations

I still think you're going to get a lot of arguments about zip(_:default:_:default:), and especially this version that takes an optional element for some reason and presumably traps (?) if you provide nil and it turns out to be the shorter of the two sequences. Are these versions significantly more efficient in an optimised build than just calling zipLongest(_:_:) and then using map and nil-coalescing or force-unwrapping to set the default?

When nil is provided for either default element, the behaviour is identical to the current zip(_:_:).

Benchmarked it and zip(_:default:_:default:) is pretty much identical to zipLongest(_:_:).map(_:) (+ ??(_:_:) / !(_:)).

From what I can see, the biggest pro of zip(_:default:_:default:) is the ease of not having to deal with Optionals. The biggest pros of zipLongest(_:_:).map(_:) are composability and having access to the other nth elements in a pure way. (Note the plurality here, higher arity zips also still exist.)

I'm also uncertain about zip(_:default:_:default:) being non-source breaking, let alone what it means for ABI stability. Introducing a Collection version of zip(_:_:) without defaults might even introduce some issues.

I would appreciate someone better versed in the intricacies of source / ABI compatibility to weigh in.

1 Like

A preliminary implementation can be found here (as a package).

Here's an overview of all available conformances on the most important types along with some preliminary motivation.

Zip2Sequence / ZipLongest2Sequence

: where Sequence1 / 2:
Sequence Sequence
LazySequenceProtocol LazySequenceProtocol

Currently, when Zip2Sequence's underlying Sequences are lazy, their laziness gets lost. By conforming to LazySequenceProtocol, we get rid of this regression.

Zip2Collection

: where Collection1 / 2:
Sequence Sequence
LazySequenceProtocol, LazyCollectionProtocol LazyCollectionProtocol
Collection Collection
BidirectionalCollection RandomAccessCollection
RandomAccessCollection RandomAccessCollection
MutableCollection MutableCollection
RangeReplaceableCollection RangeReplaceableCollection
ExpressibleByArrayLiteral RangeReplaceableCollection
ExpressibleByDictionaryLiteral RangeReplaceableCollection

Zip2Collection's protocol inheritance can roughly be split into four categories; support for: (1) laziness, (2) immutable and (3) mutable access and (4) literal initialisation.

  1. The arguments made for Zip2Sequence also stand here.

  2. The benefits of read-only access are obvious in terms of the extra performable operations and performance benefits. It also guarantees that it can be nondestructively consumed by iteration. Existing code might get performance benefits in certain unambigious situations as well "for free" since some of Sequence's operations have more performant default implementations up the hierarchy (note to self: check if this truly is the case).

  3. Zip2Collection's conformance to RangeReplaceableCollection means +(_:_:) will become available for zips.

    This will need strong use-cases.

  4. The main reason for ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral conformance is to provide an alternative shorthand for creating a default value for when Zip2Collection is Optional or empty:

    _ = Zip2Collection<[Int], [Int]>.none ?? [(42, 42)]
    _ = { $0.isEmpty ? [42: 42] : $0 }(zip([Int](), [Int]()))
    

    This will need stronger argumentation (or be dropped from the proposal). Also discussed here.

ZipLongest2Collection

: where Collection1 / 2:
Sequence Sequence
LazySequenceProtocol, LazyCollectionProtocol LazyCollectionProtocol
Collection Collection
BidirectionalCollection RandomAccessCollection
RandomAccessCollection RandomAccessCollection

ZipLongest2Collection is more limited than Zip2Collection as it can't guarantee MutableCollection and RangeReplaceableCollection's behavioural semantics (as far as I can see). The arguments made in points 1 and 2 (partly) in Zip2Collection also stand here.

I know this is a bit late to the game, but I have an alternative to present: a nil padded unbounded sequence.

That is:

[1, 2, 3].nilPadded == [1, 2, 3, nil, nil, ...]

Given this, this originally requested feature can be implemented as:

let numbers = [1, 2, 3]
let letters = ["a", "b"]
zip(numbers.nilPadded, letters.nilPadded).prefix(while: { $0 != nil || $1 != nil })
// [(Optional(1), Optional("a")),
//  (Optional(2), Optional("b")),
//  (Optional(3), nil)]

The implementation for nilPadded is shown below; no doubt others will be able to improve it out of all recognition:

extension Array {
    struct NilPaddedSequence : Sequence {
        let array: [Element]
        
        struct Iterator : IteratorProtocol {
            let array: [Element]
            var index: Int
            
            mutating func next() -> Element?? {
                defer { index += 1 }
                return index >= 0 && index < array.count ? array[index] : nil
            }
        }
        
        func makeIterator() -> Array<Element>.NilPaddedSequence.Iterator {
            return Iterator(array: array, index: 0)
        }
    }
    
    var nilPadded : NilPaddedSequence {
        return NilPaddedSequence(array: self)
    }
}
1 Like

How about:

extension Sequence {
  var nilPadded: NilPaddedSequence<Self> {
    return NilPaddedSequence(base: self)
  }
}

struct NilPaddedSequence<Base: Sequence>: Sequence {
  var base: Base
  
  func makeIterator() -> Iterator {
    return Iterator(base: base.makeIterator())
  }
  
  struct Iterator: IteratorProtocol {
    var base: Base.Iterator
    
    mutating func next() -> Base.Element?? {
      return base.next()
    }
  }
}
3 Likes

This is a very interesting approach. Thank you for sharing it.

I'm currently preparing the proposal for review. You can find the latest draft here. I'll post it up in a new thread as soon as possible for a last round of discussion.

1 Like