- Proposal: SE-NNNN
- Authors: Max Moiseev
- Review Manager: TBD
- Status: Awaiting implementation
We propose to deprecate the controversial version of a Sequence.flatMap
method
and provide the same functionality under a different, and potentially more
descriptive, name.
The Swift standard library currently defines 3 distinct overloads for flatMap
:
Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element]
where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
The last one, despite being useful in certain situations, can be (and often is) misused. Consider the following snippet:
struct Person {
var age: Int
var name: String
}
func getAges(people: [Person]) -> [Int] {
return people.flatMap { $0.age }
}
What happens inside getAges
is: thanks to the implicit promotion to
Optional
, the result of the closure gets wrapped into a .some
, then
immediately unwrapped by the implementation of flatMap
, and appended to the
result array. All this unnecessary wrapping and unwrapping can be easily avoided
by just using map
instead.
func getAges(people: [Person]) -> [Int] {
return people.map { $0.age }
}
It gets even worse when we consider future code modifications, like the one
where Swift 4 introduced a String
conformance to the Collection
protocol.
The following code used to compile (due to the flatMap
overload in question).
func getNames(people: [Person]) -> [String] {
return people.flatMap { $0.name }
}
But it no longer does, because now there is a better overload that does not
involve implicit promotion. In this particular case, the compiler error would be
obvious, as it would point at the same line where flatMap
is used. Imagine
however if it was just a let names = people.flatMap { $0.name }
statement, and
the names
variable were used elsewhere. The compiler error would be
misleading.
We propose to deprecate the controversial overload of flatMap
and re-introduce
the same functionality under a new name. The name being filteredMap(_:)
as we
believe it best describes the intent of this function.
For reference, here are the alternative names from other languages:
- Haskell, Idris
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
- Ocaml (Core and Batteries)
filter_map : 'a t -> f:('a -> 'b option) -> 'b t
- F#
List.choose : ('T -> 'U option) -> 'T list -> 'U list
- Rust
fn filter_map<B, F>(self, f: F) -> FilterMap<Self, F> where F: FnMut(Self::Item) -> Option<B>
- Scala
def collect[B](pf: PartialFunction[A, B]): List[B]
Since the old function will still be available (although deprecated) all the existing code will compile, producing a deprecation warning and a fix-it.
This is an additive API change, and does not affect ABI stability.
Ideally, the deprecated flatMap
overload would not exist at the time when ABI
stability is declared, but in the worst case, it will be available in a
deprecated form from a library post-ABI stability.
It was attempted in the past to warn about this kind of misuse and do the right
thing instead by means of a deprecated overload with a non-optional-returning
closure. The attempt failed due to another implicit promotion (this time to
Any
).
The following alternative names for this function were considered:
mapNonNil(_:)
Does not communicate what happens to nil’smapSome(_:)
Reads more like «map some elements of the sequence, but not the others» rather than «process only the ones that produce an Optional.some»filterMap(_:)
Does not really follow the naming guidelines and doesn’t seem to be common enough to be considered a term of art.