Add `filter` and `ifSome(then:)` to Optional

I propose two simple additions to Optional. They don't do anything that you couldn't before, but the improve the ergonomics of Optional, much like map and flatMap. All the names are placeholders, we can bike shed over them later.

Filter

I often find myself needing to map to conditionally map an optional to nil. E.g.

let file = trry readFile().flatMap{ try fileValidator.isValid($0) ? $0 : nil }

This code:

  • is long(er)
  • is less obvious (people are generally not very familiar with Optional.flatMap)
  • prevents you from passing in functions as closures directly (as I do with fileValidator.isValid below)
  • gets messy with duplicate try if the predicate throws

With Optional.filter, the code could be:

let file = try readFile().filter(fileValidator.isValid)

Implementation:

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

This has strong precedence from other languages:

On the flip side, it's absent in C#, which has a pretty bare bones Nullable type, which I don't think we should be taking guidance from.

ifSome(then:)

Often times, you want to do something with the value with an optional, but only if it's not nil:

let error: Error? = someError()
error.ifSome(then: NetworkLogger.log)
// or
error.ifSome { NetworkLogger.log($0) }

You could instead write:

let error: Error? = someError()
if let error = error { Logger.log(error) }

But:

  • It requires you duplicate the name of the variable, or use an arbitrary short one instead (e.g. e)
  • Prevents you from passing in functions as closures directly

You could abuse map for this syntax, but you have to explicitly discard the result, because map (rightly) isn't annotated with @discardableResult:

_ = error.map(NetworkLogger.log)

Implementation:

extension Optional {
	func ifSome<Result>(then closure: (Wrapped) throws -> Result) rethrows {
		if let wrapped = self { try _ = closure(wrapped) }
	}
}

The Result type is there for added flexibility, allowing you to support functions with discardable results (e.g. Set.remove(_:)). Most of the time, it'll just resolve to Void.

On the flip side, it's absent in Rust, Julia, OCaml, C#, F# and Haskell.

7 Likes

@AlexanderM Not commenting to the idea, I think we should let the [category] prefix for new topics fade. This was necessary on the mailing list but is completely redundant for new forum threads because there are sub-category filters and these sub-categories are assigned to the topic already. It's good that the forum allows to edit even the name of the topic. That said you can drop [Pitch]. ;)

2 Likes

+1 have added similar extensions to my code before now.

Good point, but what about those still using the mailing list interface? Should we drop them like that?

What interface do you mean? I'm not using the mailing list function of the forum, so I have no idea how threads look there. I opened up a thread for the exact same issue. Let's see how we'll proceed.

Oh I thought that the old mailing list would be kept around as an interface to the forums. I don't think that's actually the case, so nvm

As a meta comment, let's try to establish the convention of using the dedicated "heart" button instead of sending +1 replies.

I recall that this was the etiquette in the early days of the list that eventually started to be ignored, but with a dedicated button I think we stand a chance of re-establishing this etiquette.

13 Likes

This does seem a lot nicer to read.

Does it entirely fit with the mental model that Optional is a collection of 0 or 1 elements? If so, I'd be all for it. (We don't actually conform Optional to Collection in Swift, but methods such as flatMap more or less behave in a manner consistent with a scenario in which the conformance exists, for consistency I guess.)

As a point of fact, if NetworkLogger.log returns Void, then Swift does not require you to write _ = to discard the result (at least, based on my cursory fiddling this morning in a Playground).

As I and others have written elsewhere, this is actually an idiomatic use of map and not an "abuse." So I'm not so eager about this particular idea about expanding the API. It would be strictly duplicative of, again, an idiomatic use of map that we should be encouraging.

2 Likes

It's actually in Haskell, too! Because Haskell has higher kinded types, it can keep its filter in Control.Monad as mfilter:

import Control.Monad
mfilter (> 2) (Just 3) -- Just 3
mfilter (> 2) (Just 1) -- Nothing

I'm a fan of the pitch. I regularly add both of these methods to projects.

7 Likes

Idiomatic use of map comes from functional programming, no? When I read map I expect referential transparency, no side effects, and for these two lines to behave the same:

x.map(f).map(g)

x.map { g(f($0)) }

As soon as you add a side effect to the function passed to map, these expectations can break very quickly.

2 Likes

I like the proposed Optional.filter but not the Optional.ifSome(then:)

2 Likes

It's true that map is borrowed from functional programming, but Swift has no concept of 'pure' functions and has never made such a requirement for map. The Swift Programming Language itself has (or at least, one of its editions once had) an example of map(print), so I think it's fair to say that such usage is 'idiomatic' Swift.

1 Like

I imagine any such use of map is accidental and probably worth correcting. If it were the idiomatic way of executing (A) -> Void, we wouldn't also have forEach.

6 Likes

I'd give it an not-particularly-passionate yes. The ergonomics are nice; the need is not dire.

I'm not wild about the name filter for the first. I see the parallels between optionals and sequences, but it's a limited parallel. The name is just as likely to miss type errors as it is to provide useful understanding.

See previous discussion of why Optional doesn't conform to Sequence or Collection, and the downsides of reusing names between the two:

[Edit: wow, those intra-forum links are HUGE.]

About forEach: Apparently, early versions of Swift didn't have it. Why it was added is unclear to me. That said, back then, you did have to write _ = even when the return value was Void, and I wonder if that was part of the issue. Chris Eidhof, both on this list and in Advanced Swift, has argued against using forEach. So, I wouldn't say that there's general agreement as to the role of forEach in Swift. But on the flip side, I guess using map in the same role is rather controversial.

1 Like

I don't think that's a particularly useful comparison to make. Optional doesn't have map because it's a Collection, Optional and Collection are both functors. Similarly, Optional doesn't have flatMap because it's a Collection, Optional and Collection are both monads. I have body hair, not because I'm secrely a mouse, but because both mice and I are mammals.

This is the case for functions that return Void, yes, but there are functions that return results you might not care about, and might want to discard. As I note, Set.remove(_:) is an example of this. It will either remove the element from the set (returning the element), or it will do nothing (returning nil). If your interest is just to ensure the Set no longer contains the element you pass into remove(_:), then you have no use for the result, so you'll want to discard it.

7 Likes

I have no intention of make parallels between Optional and Collection, filter was strawman syntax. It could be called discard(if:), or something else. The parallel to collection was unintentional

1 Like

You're certainly right about the reasons why Optional has map and flatMap.

That said, if mice were defined solely as mammals with body hair, then you'd be a mouse. By the same token, Optional meets the semantic requirements to be a Collection; it's just not a very useful conformance, and a potentially confusing one.

If we're being pedantic, all mammals have body hair :)

Let me introduce you to the naked mole-rat:

1 Like