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 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.