Over a year later, I finally got around to cooking up a proposal from this thread. These seem quite popular, seeing as the original post has accumulated over 1.6K views. Let me know what you guys think!
API Additions to Optional
- Proposal: SE-NNNN
- Authors: Alexander Momchilov
- Review Manager: TBD
- Status: Awaiting review
During the review process, add the following fields as needed:
- Implementation: apple/swift#NNNNN
- Decision Notes: Rationale, Additional Commentary
Introduction
This proposal puts forth a few 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.
They can be considered individually, as they don't rely on eachother.
Swift-evolution thread: Discussion thread topic for that proposal
Table of Contents
Optional.filter- [
Optional.ifSome/Optional.ifNone](#Optional.ifSome/Optional.ifNone) Optional.peek
Optional.Filter
Motivation
I often find myself needing to map to conditionally map an optional to nil. E.g.
let file = try 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
Proposed solution
A new API on Optional, Optional.filter, which would let you write:
let file = try readFile().filter(fileValidator.isValid)
This has strong precedence in 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.
Detailed design
extension Optional {
/// If `self` is non-nil, and satisfies the predicate, return `self`, otherwise return `nil`.
///
/// func isEven(_ i: Int) -> Bool { return i % 2 == 0 }
///
/// let nilInteger: Int? = nil
/// let one: Int? = 1
/// let two: Int? = 2
///
/// print(nilInteger.filter(isEven) as Any) // prints "nil"
/// print(one.filter(isEven) as Any) // prints "nil"
/// print(two.filter(isEven) as Any) // prints "Optional(2)"
///
/// - Parameter shouldKeepPredicate: A closure that takes the unwrapped value,
/// and decides to either return `self` (if `true`), or `nil`.
/// - Returns:
/// * `nil` if `self` is `nil`
/// * `nil` if `self` is non-`nil` and `shouldKeepPredicate` evaluates to `false`
/// * `self` if `self` is non-`nil` and `shouldKeepPredicate` evaluates to `true`
func filter(_ shouldKeepPredicate: (Wrapped) throws -> Bool) rethrows -> Optional {
return try flatMap { try shouldKeepPredicate($0) ? self : nil }
}
}
Alternatives Considered
Naming
discard(if:)- ...
Optional.ifSome / Optional.ifNone
Motivation
Often times, you want to do something with the value with an optional, but only if it's not nil:
let error: Error? = someError()
if let error = error { Logger.log(error) }
- It requires you duplicate the name of the variable, which typically drives people to use arbitrary short names (e.g.
e) - Prevents you from passing in functions as closures directly
Another problem is that long optional chains declare a bound variable right at the start, but use it after the end. You can also never omit the name, even if it's low value enough to not justify its existence (hence the whole motivation behind implcit closure parameters, like $0, $1, ...)
if let result = Something() // "result" variable defined all the way up here
.doSomething()
.map { $0.something }
.filter({ $0.isValid }) { // can't use trailing closure syntax on last line
use(result) // result used all the way down here
}
else {
handleNil()
}
Proposed solution
New APIs on Optional, Optional.ifSome and Optional.ifNone, which would let you write:
let error: Error? = someError()
error.ifSome(then: NetworkLogger.log)
// or
error.ifSome { NetworkLogger.log($0) }
and
Something()
.doSomething()
.map { $0.something }
.filter { $0.isValid } // can use trailing closure here
.ifSome(then: { result in use(result) }) // result declared close to where it's used
.ifNone(then: handleNil)
The ifSome portion can even be simplified uses existing closure syntactic sugar:
ifSome(then: { use($0) })ifSome { use($0) }ifSome(then: use)
This has some precedence in other languages:
- Java's
Optional.ifPresent - Scala's
Option.forEach
On the flip side, it's absent in Rust, Julia, OCaml, C#, F# and Haskell.
Detailed design
The goal syntax has 3 valid forms:
-
Check only for the
somecase:anOptional.ifSome { useValue($0) } -
Check for the
nilcaseanOptional.ifNone { doSomethingInTheAbsenceOfAValue() } -
Check first for
somecase, then fornilcase:anOptional .ifSome { useValue($0) } .ifNone { doSomethingInTheAbsenceOfAValue() }
To prevent a mess of calls to ifSome and ifNone, three rules are proposed:
-
You cannot check for the
somecase twice:anOptional .ifSome { useValue($0) } .ifSome { useValue($0) } // 💥 error: value of type 'Optional<T>.IfSomeResult' has no member 'ifSome' .ifNone { doSomethingInTheAbsenceOfAValue() } -
You cannot check for the
nilcase twice:anOptional .ifSome { useValue($0) } .ifNone { doSomethingInTheAbsenceOfAValue() } .ifNone { doSomethingInTheAbsenceOfAValue() } // 💥 error: value of tuple type 'Void' has no member 'ifNone' -
If checking for both the
someandnilcase,ifSomemust come beforeifNoneanOptional .ifNone { doSomethingInTheAbsenceOfAValue() } .ifSome { useValue($0) } // 💥 error: value of tuple type 'Void' has no member 'ifSome'
Optional has two cases, some(wrapped) and none. Because they're both 4 characters long, using some/none has the asthetically pleasing effect that they makes your code line up nicely.
Implementation
extension Optional {
/// An enum used to ensure that `ifNone` is never called before `ifSome`.
enum IfSomeResult {
case some
case none
func ifNone(_ closure: () throws -> Void) rethrows -> Void {
switch self {
case .some: return
case .none: try _ = closure()
}
}
}
@discardableResult
func ifSome(then closure: (Wrapped) throws -> Void) rethrows -> IfSomeResult {
if let wrapped = self {
try _ = closure(wrapped)
return IfSomeResult.some
}
else {
return IfSomeResult.none
}
}
func ifNone(then closure: () throws -> Void) rethrows -> Void {
if case nil = self { try _ = closure() }
}
}
Alternatives considered
Loosen the rules: Allow multiple closures for each case
I don't see any benefit to syntax like this
anOptional
.ifSome { useValue($0) }
.ifSome { useValue($0) }
.ifNone { doSomethingInTheAbsenceOfAValue() }
.ifNone { doSomethingInTheAbsenceOfAValue() }
as opposed to simply:
anOptional
.ifSome {
useValue($0)
useValue($0)
}
.ifNone {
doSomethingInTheAbsenceOfAValue()
doSomethingInTheAbsenceOfAValue()
}
Loosen the rules: Allow ifNone before ifSome
I don't see any benefits to allowing this. If the previous rule is also removed, then people can interweave ifSome and ifNone, which would make for quite a mess!
Alternate name: ifNil
Although more people are familiar with nil (because it is used pretty pervasively in place of Optional.none), it's only 3 letters long, which has the effect of staggering your code:
anOptional
.ifSome { useValue($0) }
.ifNil { doSomethingInTheAbsenceOfAValue() }
as compared to:
anOptional
.ifSome { useValue($0) }
.ifNone { doSomethingInTheAbsenceOfAValue() }
However, I think people are less aware of it (because nil is used pretty pervasively).
Alternate name: ifEmpty
"Empty" is not a precedented term for an optional with no value. It's also 5 characters long, causing it to have tge same staggering issue as "nil".
"Just use Optional.map
This works, but...
-
it requires that you manually discard the result, because
Optional.mapdoes not have a@discardableResult. -
it has no "else" counterpart.
-
it's a "misuse" of what that API is intended for.
let _ = anOptional.map { useValue($0) }
ifSome(then:else:)
People have suggested combining the then and the else closures as parameters to a single function call, like so:
extension Optional {
func ifSome(
then thenClosure: (Wrapped) throws -> Void = {_ in},
else elseClosure: () throws -> Void
) rethrows {
if let wrapped = self { try thenClosure(wrapped) }
else { try elseClosure() }
}
}
This is problematic for a few reasons:
-
It doesn't "feel" as good, in any of its vareints. Compare:This isn’t as nice as my original .ifSome, ifThen, because of the look/feel when using traling closures:
someOptional.ifSome { print("Optional had a value! \($0)") } .ifNone { print("Optional was nil") }VS.
someOptional.ifSome(then: { print("Optional had a value! \($0)") }, else: { print("Optional was nil") }) // <-- "})", yuckyor
someOptional.ifSome(then: { print("Optional had a value! \($0)" }) // normal trialing closure, `else` keyword is dropped off, and key context is missing { print("Optional was nil") } -
Defaulted argument functions cause some unexpected or undesirable behaviour
-
Defaulted argument can throw, so omitting a closure can't be done without an (otherwise unecessary)
trykeyword// 💥 error: call can throw but is not marked with 'try' // ⚠️ note: call is to 'rethrows' function, but a defaulted argument function can throw someOptional.ifSome(then: { print("Optional had a value! \($0)") }) // 💥 error: call can throw but is not marked with 'try' // ⚠️ note: call is to 'rethrows' function, but a defaulted argument function can throw someOptional.ifSome(else: { print("Optional was nil") }) -
Even when we added the necessary
trykeywords, it still leads to some undesired results:// 💥 error: contextual closure type '() throws -> Void' expects 0 arguments, but 1 was used in closure body try someOptional.ifSome { print("Optional had a value! \($0)") } // ✅ Compiles, but calls the `else` case, which doesn't match expectations given what's written. try someOptional.ifSome { print("Optional was nil") } // ✅ Compiles, but it's a non-sensical no-op try someOptional.ifSome()
Fixing these two issues would require not using default args, and hand-write 3 similar functions instead:
extension Optional { func ifSome( then thenClosure: (Wrapped) throws -> Void, else elseClosure: () throws -> Void ) rethrows { if let wrapped = self { try thenClosure(wrapped) } else { try elseClosure() } } func ifSome(then thenClosure: (Wrapped) throws -> Void) rethrows { if let wrapped = self { try thenClosure(wrapped) } } func ifNone(then closure: () throws -> Void) rethrows { try closure() } } let someOptional: Int? = 1 someOptional.ifSome(then: { print("Optional had a value! \($0)") }, else: { print("Optional was nil") }) someOptional.ifSome(then: { print("Optional had a value! \($0)") }) someOptional.ifNone(then: { print("Optional was nil") })Ultimately, I don't see any benefit to combining these two closures into one function call.
-
Optional.peek
Motivation
Sometimes you might have a stream of operations on an Optional, and for debugging purposes you would like to see what's happening in the middle of the stream. You could break the stream up into multiple parts, but often times that forces you into assigning low-value names, and repeat error handling code. Suppose you had this code below, and wanted to see where you nil came from.
guard let rating = readLine()?
.map(Int.init)
.flatMap({ (1...5).contains($0) ? $0 : nil }) else {
fatalError("The input was not an integer between 1 and 5!") // Fatal error just for the example
}
You would be forced to break apart all the peaces, and repeat most of your error handling code. In this case, it gives the opportunity to make more precise error messages, but these really don't add much value beyond just "The input was not an integer between 1 and 5!".
guard let let userInputRating = readLine() else {
fatalError("There was no input!")
}
guard let userInputRatingInt = userInputRating.map(Int.init) else {
fatalError("The input was not an integer!")
}
guard let validUserInputRating = userInputRatingInt.flatMap({ (1...5).contains($0) ? $0 : nil }) {
fatalError("The input is an integer (\(userInputRatingInt)), but not between 1 and 5!")
}
Proposed solution
Add an API to Optional, Optional.peek, which lets you "peek" into what the value is, at that point in the stream. It does not give you the ability to mutate the value, because other APIs let you do that more clearly. Calls to peek can easily be added during a debugging sessions, and cleaned up afterwards, without forcing large rewrites of that area of code:
guard let rating = readLine()?
.peek { triggerBreakPoint($0) }
.map(Int.init)
.peek { triggerBreakPoint($0) }
.flatMap({ (1...5).contains($0) ? $0 : nil })
.peek({ triggerBreakPoint($0) }) else {
fatalError("The input was not an integer between 1 and 5!") // Fatal error just for the example
}
Detailed design
extension Optional {
/// Inspect the value in the optional, passing it along for further processing
/// - Parameter closure: A closure that's called with `self`.
/// - Returns: `self`.
func peek(_ closure: (Optional) throws -> Void) rethrows -> Optional {
closure(self)
return self
}
}
Alternatives considered
Alternate names:
Optional.tap- ...
Source compatibility
This is an additive change.
Effect on ABI stability
This is an additive change.
Effect on API resilience
This is an additive change.