Add Optional.filter to the Standard Library

Sure, I was trying to say that the proposal would be stronger with an example of that situation, but I couldn't immediately think of a good one.

How about fetching a value from a delegate, only if that delegate is not nil, and is also of the type you want?

let value = something.deletegate.filter { $0 is SomeClass }?.someValue

This can already be expressed good enough, at least in my opinion, as following:

input.flatMap { !$0.isOnlyWhiteSpace ? $0 : nil }

let value = something.delegate
  .flatMap { $0 as? SomeClass }?
  .someValue

That makes your implementation ultimatively to this:

public func filter(_ shouldKeep: (Wrapped) throws -> Bool) rethrows -> Optional {
  return try flatMap { try shouldKeep($0) ? $0 : nil }
}

Yes, my post mentions that. But I have a few issues with it. Firstly it's not easily discoverable. The subset of the Swift user base that participates in these discussions gives a bias as to how well adapted users are to this functional style. Sequence.flatMap is not very well known outside these inner circles, from what I can tell, let alone Optional.flatMap.

Furthermore, I don't think this is a compelling argument, because the same argument could be made for the omission of map and ??, because after all, they can be written using flatMap instead:

let a = b ?? c
let a = b.flatMap { $0 == nil ? c : $0! }

let a = b.map { $0.foo }
let a = b.flatMap { $0.foo } // Thanks to implicit promotion to optional

Well I‘m not against the general idea, I just think we need a strong motivation here, that‘s all. ;)

2 Likes

I think Swift should have this. As for motivation, I'll quote you from the previous thread:

Maybe that sounds like argument by authority, but I'd argue that this is probably a very natural thing to come up with which is why it's found in so many languages.

9 Likes

A proposal should come with proper motivations and it definitely needs to be stronger than just "this will make writing this piece of code in a more concise way" or "this is present in other languages".

By that standard, why would we have anything beyond assembly language?

if someone doesn’t know about Optional.flatMap they are definitely not going to know about Optional.filter.

1 Like

It's hard for me to understand the pushback to this. A filter method is already available on many types in the stdlib. Those types also have map/flatMap as Optional does. To me this looks like a obvious operation that Optional can and should support without confusion or undue bloat.

2 Likes

I think most people here agree and are just looking for better motivation and real world use-cases to justify this addition.

Pushbacks don't invalidate a pitch. But shouldn't this thread be moved to the Evolution/Pitches category, according to the Swift Evolution Process?

Speaking on behalf of my team, I use Optional.flatMap pretty consistently, and just about every junior dev that comes in has a real hard time understanding what flatMap has to do with the outcome. I think filter is closer to the intent of "if this optional isn't nil, then do this."

1 Like

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 array map when I want to apply a function to everything in the array. I use array filter to conditionally get rid of stuff in an array.
I use optional map because ?. doesn’t work inside parentheses. If optional filter existed i would use it to get rid of stuff in an optional. but an optional only ever has at most one thing in it, which I can already get rid of with flatMap. So there’s no benefit to the vectorized variant.
Is optional reduce next? what does optional reduce even mean?

1 Like

This is a fair point. Really I think it'd be very clear if we were able to do something like:

let a: String? = nil
a?.do { print($0) }

let a:String? = nil
a.map{ print($0) }

?

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