Hi swift-evolution!

I would like to propose the following change to the standard library:

deprecate `Sequence.flatMap<U>(_: (Element) -> U?) -> [U]` and make this 
functionality available under a new name `Sequence.filteredMap(_:)`.

The draft is available at 
https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95> and is 
included below for your convenience.

Max

Introduce Sequence.filteredMap(_:)

Proposal: SE-NNNN <https://gist.github.com/moiseev/NNNN-filename.md>
Authors: Max Moiseev <https://github.com/moiseev>
Review Manager: TBD
Status: Awaiting implementation
 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#introduction>Introduction

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.

 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#motivation>Motivation

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 getNames 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 Stringconformance 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.

 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#proposed-solution>Proposed
 solution

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]
 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#source-compatibility>Source
 compatibility

Since the old function will still be available (although deprecated) all the 
existing code will compile, producing a deprecation warning and a fix-it.

 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#effect-on-abi-stability>Effect
 on ABI stability

This is an additive API change, and does not affect ABI stability.

 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#effect-on-api-resilience>Effect
 on API resilience

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.

 
<https://gist.github.com/moiseev/2f36376c8ef4c2b1273cff0bfd9c3b95#alternatives-considered>Alternatives
 considered

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’s
mapSome(_:)
 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.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to