[Pitch] Properties and subscripts with optional getters

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:

  1. The return type of the subscript is not as precise as the intent (of type Element).
  2. 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:

  1. The bar property may be nil 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.
  2. 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

Proposed here and here.

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) { ... }
}
16 Likes

+1, I’ve found myself wanting this many times.

Without weighing in too much, I’ll also note that inout (&) has to be disallowed without having a common get and set type. Fortunately mutating methods still work thanks to optional chaining.

3 Likes

This is a very surprising statement to me. How is Optional antiquated?

7 Likes

It's an interesting pitch but it limits type difference to optionals only. Would it be feasible to explore if we can introduce set only computed property and then permit its overloading with a differently typed get only computed property with the same name?

var foo: Int { 42 }
var foo: String { set { … } }
6 Likes

The proposed syntax be clearer if the above alternative were constrained such that for a setter of type T the getter is T or T?:

var a: Int? { get { 5 } }
var a: Int { set {} } 
// âś… OK, the getter is just an optional 

var b: Int? { get { 5 } }
var b: String { set { } } 
// ❌ Cannot overload property `b` with type `String`; the only possible 
// overloads are `Int` and `Int?`.

I believe the alternative would be better because the current proposal overloads the <keyword>?-type syntax with a somewhat unclear meaning. If I saw a protocol requirement var a: Int { get? set }, I'd assume that the getter can be omitted instead of the intended meaning: that the getter returns an optional type. Further, optional getters change the type of properties very close to the type declaration; e.g. a property that declares it's a String has an optional getter that actually makes it a String?, only a single line below the type declaration. Lastly, I agree with @DevAndArtist that this feature is too specific to optionals, whereas the constrained alternative (outlined in my example above) can evolve into set-only properties and type-overloaded properties.

Optional is useful for a whole lot of non-error use cases.

9 Likes

At this point, I wonder what's the benefit of using a property (besides grouping the getter and setter with the same name)? Wouldn't you benefit from better clarity with different names?

There's a few more explorations of the surface syntax in other spellings but I think each approach has some trade-offs. Happy to take suggestions!

I can see overloading setters being somewhat useful but I'm struggling to understand how a set-only property (without a getter?) is different from a method. We'd also need to consider how key paths would work with these properties (one option is to disable key path literals on these properties where the setter and getter don't match) whereas with the optional getter approach I think there's a clear path forward.

I'm not sure I understand these questions correctly. The basic idea is that we want to use different types on the same property, but this is not possible because accessors don't have any type signature other than from the surrounding property. In your pitch you introduce keywords that apply extra logic to an accessor to swap the type into an optional one. That's cool, but it's too limited in my opinion. A simple generalization of this is by splitting the accessors apart into multiple properties with the same name.

var bar Int { get { 42 } set { ... } }

// same as 
var bar: Int { 42 }
var bar: Int { set { ... } }

We can start building upon that new form an permit in the first phase a subtype relationship between such sliced properties.

var a: SuperTypeOfA { ... }
var a: A { set { ... } }

var b: Optional<T> { ... } // Optional<T> is like a super type for `T`
var b: T { set { ... } }

Later on if we really want, we could explore if those types can be totally different.

Another benefit of this form is that you can apply different access modifiers to those properties and can have a public get-only property but an internal set-only property. Yes we can do that with public(get), but this form can permit the non existing public(set) as well. We could then say that public(get) is just syntactic sugar for such code and let the compiler generate it for us while potentially simplifying that bit on access modifiers.


I'm pretty sure there were a few cases where I really wanted to have a set-only subscript to exist.

This little code fragment by itself is enough to tell me this is not a good idea. I can no longer just look at a type definition to know if I need to unwrap a property after getting it: I now have to look at the implementation.

Even the other way around

var name: String? {
  get? { _name } // returns `String?`
  set { _name = newValue } // requires `String`
}

is problematic. I have to look at the implementation to understand why I can't set the property to nil. And by "I" I mean me and the compiler.

It seems to me also that most of your motivations are relatively trivially implemented by using functions instead of setters and subscript setters.

For example, the top fragment could be implemented as

var name: String? {
  get { _name } // returns `String?`
  
}

func set(name: String) 
{
    _name = name
}

Yes it's slightly less convenient, but I think the use-case is sufficiently rare and detrimental effects on the language significant enough for your proposal to be rejected.

3 Likes

Thanks for working on this pitch! It's a nice way of making an existing behavior of Swift (optional chaining writability) more explicitly and extensibly, and will hopefully make it easier to extend Swift's key paths with writable optional chaining and enum functionality.

@jrose Can you explain what you mean by this? Is it a matter of uppercase(&user?.name) vs. user?.name.uppercase()? If so, is there any technical reason inout couldn't extend to optional-chained values?

If you try to pass an optional-get, non-optional-set String to an inout String parameter, then you have a problem if the get actually returns nil. If you pass it to an inout String? param then you have a problem if the client attempts to set using nil.

ETA: I guess you could generalize this for optional chaining specifically, where the "out" part of inout would just not happen in the case where user is nil in uppercase(&user?.name) but it's not clear to me that that's the correct behavior in the case of a generalized 'optional-get' sort of feature.

5 Likes

Right, people have talked about generalizing optional chaining to work within parameters even independently of this pitch, but it didn’t ever feel clear enough which expressions would and wouldn’t be evaluated when you have (a) multiple parameters, and (b) a larger expression containing the call that has optional chaining parameters. So the language has left developers to use more explicit operations, like flatMap and if let.

2 Likes

I don't think the way you propose performs very well in terms of consistency.

I have an idea, I don't know if it is suitable, but I think it is relatively easy to understand.

var property: String { ... } // means none optional 
var property: String? { ... } // means optional 
var property: String?! { ... } // means optional get, none optional set 
var property: String!? { .... } // means none optional get, optional set 

...

This I don't like because you are trapping in the setter but not in the getter on out of bound errors.

To make the "trap safe" array subscript in current Swift I'd do this:

  1. getter and setter: log the "out of bounds" errors to the console.
  2. getter and setter + debug: optionally break on "out of bound errors" and allow to continue.
  3. setter only + array of non-optionals: log the "newValue == nil" error.
  4. setter only + array of non-optionals + debug: optionally break on "newValue == nil" error and allow to continue.

In other words, both out of bound error and nil element error would still be a programming errors just those will not cause the trap.

Edit: alternatively do it via a throwing subscript, something that we currently don't have in Swift (last I checked setters could not be marked throwing).

About the pitch itself I'm on the fence:

  • minus: makes language more complex
  • plus: can be useful sometimes

Oops :frowning:

Speaking of reset to nil, would it be possible to include in that proposal the ability to make the setter and only the setter optional as well? Something like this:

var property: String {
  get { ... }
  set? { ... }
}

The reasoning for this would be to support the equivalent ObjC property attribute null_resettable where the value of the property is never nil but you can set it back to the default value by setting it to nil. Couldn't we just include both in the proposal and put an error or warning that having both get? and set? in the same property is silly?

Edit: Never mind it's already mentioned in a future direction, I wish it was part of it though.

1 Like

I have to agree, getting nil back from a non-optional property seems to break the entire language semantics.

I do have the “safe” subscripting extension in several projects, but similarly adding that to Array or Collection would be preferable (more expressive) than this approach returning an optional from a non-optional function.

2 Likes

+1, tnanks for pitching this missing feature.

As for me, I would prefer the following option:

It is said in the pitch: "This approach has a trade-off where the getter and setter types specified can be arbitrarily different and may be confusing."

Let me provide some arguments that it is not misleading but can be useful in some situations. We can generalize this feature for not only optionals but for completely different types.

Lets see this example with caching of NumberFormatters instances:

struct Config: Hashable {
    let decimalSeparator: String
    let groupingSeparator: String
    let minFractionDigits: Int
    let maxFractionDigits: Int
    let roundingMode: Swift.FloatingPointRoundingRule
}

public protocol NumberFormatterType {
  func string(from number: NSNumber) -> String
}

extension NumberFormatter {
  @ThreadLockedVar private static var cachedNumberFormatters: [Config: any NumberFormatterType] = [:]

  subscript(config: Config) -> any NumberFormatterType? {
    get { ... }
    set(newValue: NumberFormatter) { ... } // set instance of Type Foundation.NumberFormatter
    // or even
    set<T: NumberFormatterType>(newValue: T) { ... }  // set instance of generic NumberFormatterType
  }
}

Another one case:

struct ErrorInfo {
  subscript(key: String) -> String? {
    get { ... } // optional string
    set(newValue: some Sendable & CustomStringConvertible) { ... } // non optional value of generic type
    // overloads
    set(newValue: String) { ... } // nonOptional String
    set(newValue: StaticString) { ... } // nonOptional StaticString
  }
}

While optional getter is general topic in this pitch, I can imagine somebody wants to use inverted variant, something like modified Dictionary subscript:

// default Dictionary subscript – both get and set are not optional
subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
  ...
}

// subscript with with `nonOptional get` and `optional set`
subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
    get { maybeValue ?? defaultValue() } // nonOptional value
    set(newValue: Value?) { ... } // optional set, can be used to remove value
}

// subscript with with `optional get` and `nonOptional set`
subscript(key: Key) -> Value? {
    get { maybeValue }
    set(newValue: Value) { ... }
}