Add Optional.filter to the Standard Library

Finally, I've had some free time to cook up a draft for my previous post. I've focussed on filter alone, because I'm afraid that lumping multiple proposals into one might cause them to all get rejected because of the disapproval of a subset.

Let me know what you guys think!

Add Optional.filter(_:) to the Standard Library

During the review process, add the following fields as needed:

Introduction

The nil coalescing operator provides the ability to inject a value into a nil optional. However, there is no current API for removing a value from an optional. This proposal introduces Optional.filter(_:) to do exactly that.

Filter takes a closure that detemines whether or not you wish to keep a value. That way, the remaining part of an optional chain can treat certain kinds of values identically to nil.

Swift-evolution thread: Discussion thread topic for that proposal

Motivation

Suppose you have some data that can be optional. Typically, part of your validation of this data would involve handling the nil case with one of the existing mechanisms (such as conditional binding, ??, !``Optional.map(_:), etc.). However, often times, there are other conditions undesirable conditions you wish to validate against, which you wish to handle in the same way.

For example, suppose getSomeInput() returns a String the user keyed in and you want to display it, and giving a nice message if the String is nil.

let userInput: String? = getSomeInput()
print(userInput ?? "No input provided")

But suppose you want to treat whitespace-only input as "No input provided". The current solution would be either quite laborious:

let input: String? = getSomeInput()
let output: String
if let input = input, !input.isOnlyWhiteSpace {
    output = input
}
else {
    output = "No input provided"
}
print(output)

... or quite non-intuitive to your average programmer:

let input: String? = getSomeInput()
print(input.flatMap { $0.isOnlyWhiteSpace ? $0 : nil } ?? "No input provided")

Proposed solution

Using the new Optional.filter(_:) API shown below, the example could be solved with:

let input: String? = getSomeInput()
print(input.filter { !$0.isOnlyWhiteSpace } ?? "No input provided")

Detailed design

The implementation is short, simple, and discoverable:

extension Optional {
    /// TODO: write documentation
    @inlinable
	public func filter(_ shouldKeep: (Wrapped) throws -> Bool) rethrows -> Optional {
		guard let some = self else { return nil }
		return try shouldKeep(some) ? some : nil
	}
}

Source compatibility

This is purely additive change.

Effect on ABI stability

None.

Effect on API resilience

This would add a new public method to Optional.

Alternatives considered

Do nothing. But this change is so lightweight and useful, I don't think it's worth passing up.

There are two approaches, which lead to different sets of viable names:

  1. The closure returning true indicates "keep the value". In this case, appropriate names include filter or keep(if:) . E.g. input.filter(isValid), input.keep { !$0.isWhiteSpaceOnly }. This is consistent with Sequence.filter(_:)

  2. The closure returning true indicates "replace the value with nil". In this case, appropriate names include reject(if:), discard(if:). E.g. input.reject(if: isInvalid), input.discard { $0.isWhitespaceOnly }

This raises 2 questions:

  1. Which is more frequently used? An isValid predicate, or an isInvalid predicate?
  2. In the case that isInvalid is a more common predicate, does the popularity of the use-case justify breaking the consistency of Sequence.filter?
8 Likes

I don't think the provided motivation/example is strong enough. There is a big difference between no and invalid values. Invalid values shouldn't be ignored implicitly.

1 Like

Sometimes they should, sometimes they shouldn't. Developers can pick whatever suits their needs.

1 Like

Perhaps clearer than any option in the proposal, if you're not interested in the difference between nil and empty or all whitespace strings:

let input = getSomeInput() ?? ""
print(input.isOnlyWhiteSpace ? "No input provided" : input)

So perhaps another example might make the proposal stronger. I'm not opposed to the idea, and I think it might be useful in some similar cases where you want to collapse all forms of invalidity to nil and avoid the overhead of creating a separate type that can only hold valid instances. I just don't have a good example in mind.

Well that presumes you have an easy way to make a dummy value that you know will fail the predicate. There's times when that's not feasible, because constructing such a dummy value is either impossible, or too expensive.

1 Like

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) }