Add Optional.filter to the Standard Library

It certainly works, though map and flatMap are transformations, which I think is why it's confusing to newer devs. I'd imagine they get a rough comment in their PR if they used Array.map to print all elements in the array instead of Array.forEach.

Reading above, I'm now realizing I misread what Alexander pitched in the first place, and am talking about a different problem, which is needing a better shorthand for the opposite of ?? (i.e. "if item is not nil, then do this) than .flatMap or .map on optional.

1 Like

The argument that Optional.flatMap invalidates the need for Optional.filter is not compelling; by that logic, Sequence.filter is unnecessary by Sequence.compactMap or even Sequence.flatMap.

// The following are functionally equivalent:
sequence.filter(shouldKeep)
sequence.compactMap { shouldKeep($0) ? $0 : nil }
sequence.flatMap { shouldKeep($0) ? [$0] : [] }   

filter holds notable advantages over performing the same operation using flatMap:

  • Because filter does not perform a transformation on the wrapped value, call site clarity improves. Upon reading filter, the intent of the operation is clear: the wrapped value is either kept or discarded; it is not transformed.
  • Using flatMap as filter typically involves the use of the ternary operator, which increases cognitive load.
    For example, which of the following is easier to read at a glance?
let effectiveText = textField.text.flatMap { !$0.isEmpty ? $0 : nil }
let effectiveText = textField.text.filter { !$0.isEmpty } 

The gain of Optional.filter exists principally on the grounds of readability, not on difficulty of implementation, and its inclusion in the Standard Library should be judged as such.

6 Likes

Is optional reduce next? what does optional reduce even mean?

This topic is only tangentially related, so I won't go into much detail here. In short, reduce does exist on Optional. It looks like this:

func reduce<Output>(
    _ initialValue: Output,
    _ combine: (Output, Wrapped) throws -> Output
) rethrows -> Output {
    switch self {
    case .some(let value):
        return combine(initialValue, value)
    case .none:
        return initialValue
    }
}

For instance, if you have an Int and an Optional<Int> and you'd like to take the minimum of the two (or just use the first one if the second is nil), you might write
let minimum = optionalInt.reduce(nonOptionalInt, min)

5 Likes

I think of them as the same. A map turns an array of Foo into an array of Bar. Or an optional Foo into an optional Bar.

flatMap does the same, except it also removes a layer through the transform. Same for Promises, Result, etc. I see no difference.

In fact, I hope we some day can just declare them all to be conforming to some common protocol, wether we call that protocol Monad or Mappable or whatever, I don’t care too much about.

In fact, we could then implement filter on a default protocol extension through the confirming type’s flatMap implementation, as long as we also had a static empty element defined.

Array’s filter could easily be implemented as

func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
    return flatMap { isIncluded($0) ? [$0] : [] }
}
6 Likes

i was about to fight but actually that sounds like a pretty useful operation. if filter does end up getting added to optional we should add reduce too

I feel like the original pitch was much more to the point. I don’t think it’s necessary to muddy the waters with excessive motivation discussion and alternative considered sections that only invite confusion. This pitch isn’t about inventing a novel solution to Swift specific problem. This is about including the batteries. :battery:
@AlexanderM I advise you significantly down pair the proposal: Short description and reference to the plethora of prior art is all that’s needed here.

5 Likes

I like to think of Optional as a collection of zero or one elements, so filter fits perfectly in my mental model. +1

Well, I guess I'm suggesting it doesn't need extensive effort to justify, if we agree that this is a simple and obvious addition of "batteries" (as Pavol aptly put it).

okay this is not exactly related to the proposal which i am /slightly/ in favor,, but can i just say the whole batteries thing has been taken way out of context. Optional.filter is not a battery, it’s just sugar (which I think could still stand on its own). Batteries are common things that are hard to do (correctly) yourself and would benefit from a standard implementation. Examples of missing batteries in Swift include

  • Printing a floating point number to however many decimal places
  • Generating a random vector
  • Trimming whitespace from a string
  • Splitting a string into lines

Optional.filter is not a battery.

2 Likes

am i the only one here who sees Array.map , Array.flatMap , and Array.filter as distinct from Optional.map and Optional.flatMap ?

I use optional map because ?. doesn’t work inside parentheses.

This is close to what I was thinking.

I consider Optional.map to be a hacky mechanism for using functions after an instance, in a way that isn't possible for non-optional types, which require the function before an instance.

var instance: Class?

instance = instance.map {
  $0.property1 = "😺"
  $0.property2 = "🐈"
  return $0
}

instance = {
  $0.property1 = "😺"
  $0.property2 = "🐈"
  return $0
} ( Class() )

Additionally, Optional can be used as a hack around not being able to extend Any. That's what I see from this proposal: that all of us who would like this, actually want filter for all types, not just optionals (though the name would probably have to be altered, to avoid ambiguity for multidimensional sequences). Unfortunately, you can't do that now without extending all the types you're going to use, to adopt a protocol, like this:

protocol Filterable { }

extension Filterable {
  func filter(_ shouldKeep: (Self) throws -> Bool) rethrows -> Self? {
    return try shouldKeep(self) ? self : nil
  }
}

extension String: Filterable {}

"🌫".filter { $0.isOnlyWhiteSpace } ?? ""

let string: String? = nil
string?.filter { $0.isOnlyWhiteSpace } ?? ""

I think it would be a much better idea to shelve this proposal, work on making it possible to extend Wrapped (i.e. Any), and then add more functions to Any directly. Otherwise, as Optional grows more powerful, people are eventually just going to stop using non-optionals entirely.

1 Like

Wat?

2 Likes

This is the consequence of years of exposition to fluent interfaces, pipes, functional idioms... and the fact that if is a statement. People like linear imperative code:

let result = start.one.two().three { ... }.four { ... }

vs.

let result = Four(Two(start.one).three)
1 Like

I cannot follow your reasoning. When every other language with Optional includes this method and Swift doesn’t, this is about :battery:!

1 Like

Personally, I would put Optional.filter(_:) into the "complete the API of the Standard Library types". It is hard to argue that there is similarity between sequences and optionals, and even thought I would disagree with adding Optional.forEach(_:), not having a filter is an omission.

Having said that, there is one important thing that, I believe, the proposal is missing. Since this is such a no-braner for a lot of people, I wouldn't be surprised if there is already code out there that extends Optional adding filter. This code will break. Whether it's a problem or not, it is worth mentioning in the proposal.

Optional.filter can be implemented easily with existing constructs, just in a less clear and slightly more confusing way. so, not a battery

1 Like

I can only spot one in the compatibility suite (in Kickstarter-Prelude) FWIW.

2 Likes

Personally, I disagree with that characterisation of "battery" vs. "sugar". Sugar for me is something that is purely notational shorthand, e.g. like optional chaining is completely equivalent to the use of map and flatMap. There are no new semantics there. Filter is new semantics.

1 Like

Sure. I totally agree. I don't agree that this is equivalent to "all of us who would like this, actually want filter for all types". That's not really possible to infer from neither the proposal, nor this discussion.

I'm with you.

EDIT: But I find it fascinating to witness this kind of sparkles. They're the signs of, yes, joy of coding :-)

1 Like

I love this. I'm not sure if it's just because I read this yesterday morning, but yesterday I wrote something where I would have used this. I had an optional array and I wanted to convert .some([]) into nil. I used flatMap { $0.isEmpty ? nil : $0 }, but that felt much harder to read and write than filter { !$0.isEmpty } would have been.

3 Likes