Hey folks, I'm sharing an early draft of a pitch for adding optional getters to Swift. Its use is similar to updating a property through optional chaining syntax. Appreciate the early feedback as this pitch gets refined!
Properties and subscripts with optional getters
Introduction
Computed properties and subscripts in Swift require both the getter and setter to be of the same type. This is intuitive and sufficient for most cases. However, there are times where we might prefer that the getter be optional and the setter be non-optional.
This proposal introduces a way to qualify a getter in a computed property or subscript so it returns an Optional
of the said type while the setter remains non-optional.
var name: String {
get? { _name } // returns `String?`
set { _name = newValue } // requires `String`
}
Motivation
There are several use-cases why this feature might be considered useful.
More precise APIs
Allow properties that can't be reset to nil
One of the key reasons for this change is that it would enable for more precise APIs where the intent that is a field may start off with a nil
value but should never allow resetting back to nil
. This use case and others are further elaborated on below. This has be suggested most recently here and here.
var name: String {
get? { _name } // returns `String?`
set { _name = newValue } // requires `String`
}
In the above example, a consumer might choose to read name
before it has been set, in which case it returns nil
. Once set, the property returns a non-nil
value thereafter.
Returning elements safely from collections
While Swift.Array
allows subscripting into its elements in an unsafe way (that traps at runtime), authors of other collection types may choose to vend an optional value of the element instead.
Some collection implementations may include safe access to their elements. For example, a potential implementation for Array
might look like this:
extension Array {
subscript(safe index: Int) -> Element? {
get {
if index >= 0, index < count {
return self[index]
} else {
return nil
}
}
set {
if let index, let newValue { self[index] = newValue }
else {
// no-op? trap at runtime? remove element from collection?
}
}
}
}
This implementation poses several open API design challenges:
- The return type of the subscript is not as precise as the intent (of type
Element
). - The setter is forced to be optional even if we wanted a version where the consumer could not change the number of elements (such as in a fixed-size buffer).
With optional getters, we could imagine a syntax like:
extension GrowingOnlyCollection {
subscript(index: Int) -> Element {
get? {
if index >= 0, index < count {
return storage[index]
} else {
return nil
}
}
set {
storage[index] = newValue
}
}
}
This enables us to keep a concise declaration for subscripts that communicates the intented type of its Element
but yet allows for returning it safely by wrapping the result in an optional.
Previous discussions of returning "safe" values include this and this
Enum properties
Vending computed properties for enum
cases are also a frequent approach to improving the ergonomics of enums in Swift (even being a motivating example in WWDC 2023's Platforms State of the Union with the CaseDetection
macro). One might write an extension as follows (potentially even with a macro):
enum A {
case foo(String)
case bar(Int, Double)
}
extension A {
var foo: String? {
get {
guard case .foo(let value) = self else { return nil }
return value
}
set {
guard let newValue else { return }
self = .foo(newValue)
}
}
var bar: (Int, Double)? {
get {
guard case .bar(let a, let b) = self else { return nil }
return (a, b)
}
set {
guard let newValue else { return }
self = .bar(newValue.0, newValue.1)
}
}
}
This approach suffers from the fact that setting the property to nil
is quite nonsensical and there is not a clear well-defined behavior that would be expected. In the above example, we have chosen to do nothing (one could imagine other alternatives like trapping or producing an assertionFailure
to alert the caller of a programmer error.)
A more precise API would disallow this in the first place, and this is where the ability to have an optional getter but non-optional setter would be very useful:
extension A {
var foo: String {
get? {
guard case .foo(let value) = self else { return nil }
return value
}
set {
self = .foo(newValue)
}
}
var bar: (Int, Double) {
get? {
guard case .bar(let a, let b) = self else { return nil }
return (a, b)
}
set {
self = .bar(newValue.0, newValue.1)
}
}
}
Writable key paths with optional chaining (and subsequently, case paths)
Today's key paths have a quirky side-effect where they become less useful once they pass through an optional chaining boundary.
struct Foo {
var bar: Bar?
}
struct Bar {
var name: String
}
\Foo.bar // returns WritableKeyPath<Foo, Bar?>
\Foo.bar? // returns KeyPath<Foo, Bar?>
\Foo.bar?.name // returns KeyPath<Foo, String?>
The KeyPath
subclass no longer allows writing to the property:
var a = Foo()
a[keyPath: \.bar?.name] = "test" // Cannot assign through subscript: key path is read-only
This is problematic for several reasons:
- The
bar
property may benil
in which case the behavior is not clearly defined. One could imagine a reasonable behavior is to trap at runtime, as it would be a programmer error. - We don't have the ability to allow writes to this key path be non-optional and still return optional values when read, in a behavior similar to regular optional chaining.
var a = Foo()
a.bar?.name // returns `String?`
a.bar?.name = "test" // assigned value must be `String` not `String?`
If we had the ability to specify an optional getter but non-optional setter, we'd be able to create a type of key path that allows writing through an optional chaining boundary.
extension Any {
// today's `KeyPath`
subscript<V>(keyPath: KeyPath<Self, V>) -> V {
get { ... }
}
// today's `WritableKeyPath`
subscript<V>(keyPath: WritableKeyPath<Self, V>) -> V {
get { ... }
set { ... }
}
// new `WritableOptionalPath`
subscript<V>(keyPath: WritableOptionalPath<Self, V?>) -> V {
get? { ... }
set { ... }
}
}
Similarly, if we eventually introduce the analogous version of key paths for enum access (past attempts), we would need a similar subscripting syntax that reads an optional and writes a non-optional value:
enum B {
case foo(String)
case bar
}
var b = B.foo
b[keyPath: \B.foo] // returns `String?`
b[keyPath: \B.foo] = "test" // assigned value must be `String` not `String?`
Detailed Design
To be documented later
Implications on key paths
Because this proposal introduces versions of properties and subscripts that have optional getters, it would be consistent to simultaneously introduce a version of a key path that can provide read and write access to these fields.
Additionally, this writable optional key path is exactly the functionality we would need to support writable key paths through an optional chaining boundary (as discussed above.)
Because key path literals would need to return a new subclass that supports this functionality, this is a non ABI-stable change.
Future directions
Case paths
With optional getters in place (and their responding key path subclass), it could be time to tackle another extension to key paths to support enums. This is an insanely popular request on the forums, with discussions here, here, here, here and here dating back to 2018.
Protocol requirements
Another area of consideration is if a protocol could require properties to have optional getters.
protocol View {
var tintColor: Color {
get? set
}
}
Furthermore, another open question is if properties with non-optional getters can fulfill such a protocol requirement (where access through the protocol would automatically wrap the result in an Optional
.)
extension SomeView: View {
var tintColor: Color
}
Tangentially there's also interest in having non-optional fields implicitly satisfy optional requirements.
Optional setters
Another orthogonal direction is the exploration of optional setters and non-optional getters. This is most useful if a property should have some default value and setting it to nil
may "reset" its value back to the previous default.
var tintColor: Color {
get { ... }
set? { ... }
}
Alternatives considered
Only allow optional getters on subscripts, not properties
In this variant, we only support optional getters on subscripts where we expect to see the most use of this feature. This leaves the possibility of adding the feature to computed properties open in the future.
Allow standalone getter/setter declarations
var property: String? {
get { ... }
}
var property: String {
set { ... = newValue }
}
subscript(index: int) -> String? {
get { ... }
}
subscript(index: int) -> String {
set { ... = newValue }
}
One of the key motiviations for declaring setters without getters is to allow overloading declarations for setters. This allows additional setter (possibly generic) declarations to be provided without needing to provide the corresponding getters (which can then be ambiguous.)
This approach has a trade-off where the getter and setter types can be arbitrarily different. While it is the most general approach, it is unclear that it would support a variety of use cases to warrant its addition.
Other spellings
Option 1 (proposed): ?
suffix
This option is most similar to the suffix of an Optional
type when spelt with the shorthand syntax and also alludes to the trailing suffix that key path literals allow. It also has the advantge of not having to introduce new keywords.
var property: String {
get? { ... }
set { ... }
}
subscript(index: int) -> String {
get? { ... }
set { ... }
}
Option 2: optional
modifier
This option maximizes clarity by introducing an optional
modifier to be placed before get
. optional
already a keyword when used in @objc
protocol requirements.
var property: String {
optional get { ... }
set { ... }
}
subscript(index: int) -> String {
optional get { ... }
set { ... }
}
Option 3: nonoptional
modifier
This approach introduces a nonoptional
keyword and already has some precedence with the nonmutating
modifier on getters. It has a disadvantage of being a negative qualifier and requiring the return type to be defined with an Optional
.
var property: String? {
get { ... }
nonoptional set { ... }
}
subscript(index: int) -> String? {
get { ... }
nonoptional set { ... }
}
Option 4: Specify the setter's parameter type
In this approach, we allow specifying a parameter list after the set
keyword similar to how one might use it on willSet
.
This approach has a trade-off where the getter and setter types specified can be arbitrarily different and may be confusing. While the compiler could enforce that the getter and setter are related types, the syntax may be overly general.
var property: String? {
get { ... }
set(newValue: String) { ... }
}
subscript(index: int) -> String? {
get { ... }
set(newValue: String) { ... }
}