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.isValidbelow) - gets messy with duplicate
tryif the predicatethrows
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:
- Java's
Optional.filter - Scala's
Option.filter - Rust's
Option.filter - F#'s
Option.filter - OCaml's
Option.filter - Julia's
Nullable.filter - Haskell's Monad's
mfilter
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.
- Java's
Optional.ifPresent - Scala's
Option.forEach
On the flip side, it's absent in Rust, Julia, OCaml, C#, F# and Haskell.