SE-0222: Lazy CompactMap Sequence
* Proposal: [SE-0222](0222-lazy-compactmap-sequence.md) * Authors: [TellowKrinkle](https://github.com/TellowKrinkle), [Johannes Weiß](https://github.com/weissi) * Review Manager: [John McCall](https://github.com/rjmccall) * Status: **Rejected** * Implementation: [apple/swift#14841](https://github.com/apple/swift/pull/14841) * Decision Notes: [Rationale](https://forums.swift.org/t/se-0222-lazy-compactmap-sequence/14850/16)
Introduction
Chaining multiple .map()s and .filter()s on a lazy collection leads to suboptimal codegen, as well as large, painful type names. To improve this, we propose adding a LazyCompactMap{Sequence, Collection} type along with some overloads on the other lazy collection types' .map(:) and .filter(:) functions which return this type to get better codegen and shorter type names.
Swift-evolution thread: Discussion thread topic for the proposal
Motivation
The current lazy system is very good for easily defining transforms on collections, but certain constructs can lead to less-than-optimal codegen.
For example, the code collection.map(map1).filter(filter1).map(map2).filter(filter2) will lead to code like this in the formIndex(after:) method:
do {
do {
collection.formIndex(after: &i)
} while !filter1(map1(collection[i]))
} while !filter2(map2(map1(collection[i])))while it could be represented with this more efficient single loop:
do {
collection.formIndex(after: &i)
} while !filter1(map1(collection[i])) && !filter2(map2(map1(collection[i])))Currently, you can get a single loop by instead using this compactMap:
collection.compactMap {
let a = map1($0)
guard filter1(a) else { return nil }
let b = map2(a)
guard filter2(b) else { return nil }
return b
}but this removes the nice composability of the chained map/filter combination.
The standard library recently got an override on LazyMapCollection and LazyFilterCollection which combines multiple filters and maps in a row, however it does not work with alternating maps and filters.
Proposed solution
Define a LazyCompactMapCollection collection (and sequence) which represents a compactMap. Then, add overrides on LazyMapCollection.filter, LazyFilterCollection.map, Lazy*Collection.compactMap, and LazyCompactMapCollection.{filter, map} to return a LazyCompactMapCollection that combines all the maps and filters.
As an added bonus, you’ll never see a giant chain of LazyMapCollection<LazyFilterCollection<...>, ...> again
Detailed design
A new LazyCompactMapCollection and equivalent Sequence should be defined like so:
public struct LazyCompactMapCollection<Base: Collection, Element> {
internal var _base: Base
internal let _transform: (Base.Element) -> Element?
internal init(_base: Base, transform: @escaping (Base.Element) -> Element?) {
self._base = _base
self._transform = transform
}
}with a very similar set of overrides to the current LazyFilterCollection
Then, the following extensions should be added (with equivalent ones for Lazy Sequences):
extension LazyMapCollection {
public func compactMap<U>(_ transform: @escaping (Element) -> U?) -> LazyCompactMapCollection<Base, U> {
let mytransform = self._transform
return LazyCompactMapCollection<Base, U>(
_base: self._base,
transform: { transform(mytransform($0)) }
)
}
public func filter(_ isIncluded: @escaping (Element) -> Bool) -> LazyCompactMapCollection<Base, Element> {
let mytransform = self._transform
return LazyCompactMapCollection<Base, Element>(
_base: self._base,
transform: {
let transformed = mytransform($0)
return isIncluded(transformed) ? transformed : nil
}
)
}
}
extension LazyFilterCollection {
public func compactMap<U>(_ transform: @escaping (Base.Element) -> U?) -> LazyCompactMapCollection<Base, U> {
let mypredicate = self._predicate
return LazyCompactMapCollection<Base, U>(
_base: self._base,
transform: { mypredicate($0) ? transform($0) : nil }
)
}
public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyCompactMapCollection<Base, U> {
let mypredicate = self._predicate
return LazyCompactMapCollection<Base, U>(
_base: self._base,
transform: { mypredicate($0) ? transform($0) : nil }
)
}
}
extension LazyCompactMapCollection {
public func compactMap<U>(_ transform: @escaping (Element) -> U?) -> LazyCompactMapCollection<Base, U> {
let mytransform = self._transform
return LazyCompactMapCollection<Base, U>(
_base: self._base,
transform: {
guard let halfTransformed = mytransform($0) else { return nil }
return transform(halfTransformed)
}
)
}
public func map<U>(_ transform: @escaping (Element) -> U) -> LazyCompactMapCollection<Base, U> {
let mytransform = self._transform
return LazyCompactMapCollection<Base, U>(
_base: self._base,
transform: {
guard let halfTransformed = mytransform($0) else { return nil }
return transform(halfTransformed)
}
)
}
public func filter(_ isIncluded: @escaping (Element) -> Bool) -> LazyCompactMapCollection<Base, Element> {
let mytransform = self._transform
return LazyCompactMapCollection<Base, Element>(
_base: self._base,
transform: {
guard let halfTransformed = mytransform($0), isIncluded(halfTransformed) else { return nil }
return halfTransformed
}
)
}
}Source compatibility
In Swift 5, while most code will work with the new extensions, code that relies on the return type of LazyCollection.compactMap(_:) will break.
In addition, code like following code will break:
let array = [0, 1, 22]
let tmp = array.lazy.map(String.init).filter { $0.count == 1 }
let filtered: LazyFilterCollection<LazyMapCollection<[Int], String>> = tmpHowever, this type of code is probably rare and similar code will already be broken by the previously mentioned change that coalesces .filter(:).filter(:) and .map(:).map(:)
Effect on ABI stability
N/A
Effect on API resilience
N/A
Alternatives considered
The main alternative would be to not do this at all. This alternative isn't great, as it can be many times slower when the map/filter functions do little work, as shown by this test