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.isValid
below) - gets messy with duplicate
try
if 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
some
case:anOptional.ifSome { useValue($0) }
-
Check for the
nil
caseanOptional.ifNone { doSomethingInTheAbsenceOfAValue() }
-
Check first for
some
case, then fornil
case: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
some
case 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
nil
case twice:anOptional .ifSome { useValue($0) } .ifNone { doSomethingInTheAbsenceOfAValue() } .ifNone { doSomethingInTheAbsenceOfAValue() } // 💥 error: value of tuple type 'Void' has no member 'ifNone'
-
If checking for both the
some
andnil
case,ifSome
must come beforeifNone
anOptional .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.map
does 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") }) // <-- "})", yucky
or
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)
try
keyword// 💥 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
try
keywords, 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.