The fact that map, filter, reduce etc return arrays by default is a consistent point of frustration for myself and many other users. Having to jump through a bunch of hoops just to avoid unnecessary intermediate arrays with .lazy all over the place is annoying at best and hazardous at worst (especially for large collections).
Just a couple of weeks ago @tkrajacic posted hoping for a way to easily map from a Set to a Set which unfortunately isn't really possible without Higher Kinded Types. That said, we could still make the current APIs much more ergonomic and mapping sequences lazy by default.
I propose adding a SequenceInstantiable protocol to the standard library which is defined as follows:
protocol SequenceInstantiable: Sequence {
associatedtype SequenceElement
init<S>(_ s: S) where S: Sequence, S.Element == SequenceElement
}
We could then make Array, Set, Dictionary and all of the other concrete standard library sequence types implement it in extensions:
extension Array: SequenceInstantiable {
typealias SequenceElement = Element
}
extension Set: SequenceInstantiable {
typealias SequenceElement = Element
}
extension Dictionary: SequenceInstantiable {
typealias SequenceElement = (Key, Value)
init<S>(_ s: S) where S: Sequence, S.Element == (Key, Value) {
self.init(uniqueKeysWithValues: s)
}
}
This would allow us to provide a map(into:) function where you can feed an arbitrary sequence into a type's constructor:
extension Sequence {
func map<S>(into: S.Type) -> S where S: SequenceInstantiable, S.SequenceElement == Element {
return S(self)
}
}
This function would allow us to chain lazy functional operators and then finally pipe it into the desired type:
let mappedValues = setOfValues.lazy
.map { ... }
.filter { ... }
.map(into: Set.self)
I'd also, perhaps more controversially, like to propose making map, filter et al lazy by default. I.e. we'd replace them with, essentially:
extension Sequence {
func newFilter(_ isIncluded: @escaping (Element) -> Bool) -> LazyFilterSequence<Self> {
return self.lazy.filter(isIncluded)
}
func newMap<U>(_ transform: @escaping (Element) -> U) -> LazyMapSequence<Self, U> {
return self.lazy.map(transform)
}
}
I believe making these functions lazy by default will save engineers from more bugs than any additional frustration this might cause. We can maintain almost complete source compatibility by writing another extension:
extension Sequence {
func newMap<S, U>(_ transform: @escaping (Element) -> U) -> S where S: SequenceInstantiable, S.SequenceElement == U {
return S(self.lazy.map(transform))
}
}
Which will provide the identical behavior to today's map assuming the type can be inferred to be an array. If the type cannot be inferred to be an array, the Xcode source migrator could add an explicit array type annotation.
Aside: While playing around with this in a playground, I noticed that the current lazy sequence map, filter etc do not take throwing transforms, while the regular ones do. I wasn't sure if this was an oversight or if this was intentional.