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 Optional
s. 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 zip
s 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 default
s might even introduce some issues.
I would appreciate someone better versed in the intricacies of source / ABI compatibility to weigh in.
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 Sequence
s 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.
-
The arguments made for
Zip2Sequence
also stand here. -
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). -
Zip2Collection
's conformance toRangeReplaceableCollection
means+(_:_:)
will become available forzip
s.This will need strong use-cases.
-
The main reason for
ExpressibleByArrayLiteral
andExpressibleByDictionaryLiteral
conformance is to provide an alternative shorthand for creating a default value for whenZip2Collection
isOptional
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)
}
}
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()
}
}
}
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.