Set-only subscripts

Introduction

Currently, subscripts can be read-only or they can be read-write, but there is no way to declare a subscript as write-only. Write-only subscripts have a number of important uses, and this proposal aims to bring them into the language.

Motivation

The MutableCollection protocol overrides the Range subscript from Collection to allow assignment. However, because the return type of that subscript is SubSequence, it follows that the only thing which can be assigned through that subscript is another SubSequence:

var nums = Array(0..<5)
var x = [6, 7, 8]
nums[1..<4] = x       // error
nums[1..<4] = x[...]  // success

Conceptually, MutableCollection allows the one-to-one replacement of individual elements, so it ought to be possible to assign any Sequence of Elements through that subscript, provided the lengths and element types are the same.

We can write a generic subscript to allow assigning any sequence to a range, and the corresponding version for RangeExpression as well:

Generic subscript implementation
extension MutableCollection {
  /// - Precondition: The number of elements must not change.
  subscript<S: Sequence>(_ range: Range<Index>) -> S
    where S.Element == Element
  {
    get { fatalError() }
    set {
      // TODO: Validate the precondition
      var i = range.lowerBound
      for x in newValue {
        self[i] = x
        formIndex(after: &i)
      }
    }
  }
  
  /// - Precondition: The number of elements must not change.
  subscript<R: RangeExpression, S: Sequence>(_ range: R) -> S
    where R.Bound == Index, S.Element == Element
  {
    get { fatalError() }
    set { self[range.relative(to: self)] = newValue }
  }
}

But there are some problems.

First, we had to use fatalError() in the getters, because there is no way to construct a generic Sequence from a slice of our collection. Indeed, we don’t want that anyway, we just want the setter.

Second, having both the generic RangeExpression subscript and the current version exist simultaneously, produces “Ambiguous use of subscript” errors when calling the getter. Strangely, when the return-type is unspecified, there is no error, but when the return type is constrained to exactly match the existing subscript there is a compiler error for ambiguity:

var a = nums[2...4]   // success
a = nums[2...4]       // error: ambiguous use of subscript
let b: ArraySlice<Int> = nums[2...4]  // error: ambiguous use of subscript

These difficulties would disappear if we could declare the generic subscripts as set-only. Then the only getter would be the one from the basic Collection subscript, and everything would work as desired with no ambiguity and no danger of hitting the fatalError in the generic getter.

Proposed solution

The proposed solution is to allow set-only subscripts.

This will make it possible to write the generic MutableCollection assignment functionality described in the motivation section. It will also allow similar use-cases for other types, such as a Matrix which has a dedicated Column type: one subscript would be get-only and return a Column, while another would be set-only to allow the assignment of any Sequence to a column, provided the lengths and element types match.

The same approach would work with the currently-in-review proposal for discontiguous collection operations: the MutableCollection subscript taking a RangeSet could become generic and allow the assignment of any Sequence with matching length and element type, instead of being restricted to only DiscontiguousSlice.

Subscripts often exhibit this duality, where the getter should return a specific type and the setter should be generic. Set-only subscripts will enable this pattern in Swift.

Alternatives considered

One possible option is to introduce the access modifier private(get). This would not truly make the subscript set-only, but it would allow a set-only API to be published.

An expansion of the proposal could allow set-only computed properties as well.

Related discussion

Set-only computed properties (last post July 18, 2018)

22 Likes

I personally find set-only properties or subscripts as a strange idea, but I'm neutral on having those in the language. So if it gets pushed through review, fine by me. Maybe one day I'd have a use case for those, but I never had the need for them as for right now. We'll see. :slight_smile:


Btw. wouldn't we also need a series of new *KeyPath types to reflect the set only capabilities? (Not part of this proposal, but as a following implication of this.)

3 Likes

I'm a little bit lost about the core problem that is being solved here, probably because I don't write new subscripts very often.

  • Is the problem that the compiler is unable to infer the right subscript to use, so having an additional kind of subscript is helping it disambiguate?
  • Is the problem that the existing Collection subscript is not sufficiently general for what you are trying to do?
  • Are there data types for which it makes sense to have a write-only subscript but not a read-write subscript?

Set-only subscripts would also be useful for correctness of a bunch of low-level stuff. It would be nice to be able to write a function that takes a set-only view as an argument and have the compiler statically guarantee that you don't introduce a bug that inadvertently reads from your output buffer.

17 Likes

Indeed, it's a bit strange, but a need does occasionally come up! Here's some of the motivation I cite in my original post:

There's already some precedent in the language for this kind of strangeness: nonmutating set:

var someExternalStorage = 0

struct SomeStruct {
    var someProperty: Int {
        get { return someExternalStorage }
        nonmutating set { someExternalStorage = newValue }
    }
}

let s = SomeStruct()

// Without `nonmutating`, this would require `s` to be a `var`,
// even though `s` isn't being mutated.
s.someProperty = 1

print(s.someProperty) // => 1
6 Likes

The motivation in the pitch is that certain subscripts should return a specific concrete type from their getter (eg. Slice<Self>), but should allow the assignment of a generic type through their setter (eg. S: Sequence where S.Element == Element).

The simplest way to write that in Swift is to have two separate subscripts: one get-only and one set-only. It would be a significantly larger undertaking to allow the return type from the getter to be different from the newValue type in the setter of a single subscript declaration.

Furthermore, there are additional scenarios indicated by Steve Canon and Alexander Momchilov where set-only functionality is inherently desirable in its own right.

13 Likes

Thanks. This concise summary:

The motivation in the pitch is that certain subscripts should return a specific concrete type from their getter (eg. Slice<Self> ), but should allow the assignment of a generic type through their setter (eg. S: Sequence where S.Element == Element ).

The simplest way to write that in Swift is to have two separate subscripts: one get-only and one set-only. It would be a significantly larger undertaking to allow the return type from the getter to be different from the newValue type in the setter of a single subscript declaration.

makes it clearer to me what the main issue is.

1 Like

So finally we could have a getter that returns Optional and a setter that forbids nil? :-)

29 Likes

Okay I'm intrigued by this overloading now. +1 now. This is convincing enough for me.

4 Likes

The overloading technique could be used in property wrappers that would access the EnclosingSelf. There are situations where the get and set types 'must' differ because it wouldn't work otherwise. Now when I think about it. Every time when I have split a subscript into two methods because of that reason, I ultimately needed this feature!

6 Likes

This is intriguing to me. Just hope that it somehow would be possible to use the independently defined getter + setter for mutations.

I’m not sure how that would work, but I also haven’t given it much thought. But it would be a shame if we had to chose between asymmetric setter/getter for correctness on the one hand, and a strictly incorrect symmetric setter/getter for ergonomics on the other hand.

2 Likes

Question to the language experts: Would this feature also allow for inout parameters in subscripts?

struct S {
  static subscript <T>(value: inout T) -> T {
    set {
      value = newValue
    }
  }
}

Not by the proposal implementation itself, but as some followup feature.


I would need a combination of the pitched feature and the above:

public subscript (context: Context, storage: Storage) -> Value {
  get {
    storage.value
  }
}

public subscript (context: Context, storage: inout Storage) -> Value {
  set {
    storage.value = newValue
  }
}

This would allow a nice optimization for property wrappers on value types, while keeping the design nearly the same for reference types. :+1:


If the answer to the above question is 'Yes' then I think it's another good motivation for this proposal. :wink:

1 Like

If that motivation holds for this proposal to push it through then it might also be a good motivation for set-only properties?

protocol P {
  var value: Int? { get }
  var value: Int { set }
}
6 Likes

I have definitely wanted this a few times. If nothing else, it would let us get a nicer Swift overlay for WatchKit WKInterfaceElements that don’t allow for reading the value. So we could go from this:

class WKInterfaceLabel: WKInterfaceObject {
	…
	func setText(_ text: String?)
	…
}

to this:

class WKInterfaceLabel: WKInterfaceObject {
	…
	var text: String? { set }
	…
}
14 Likes

A more general solution would be to allow multiple properties with the same name but different types. There's an example of this sort of thing in UITextField with its text property. var text: String? It can be assigned a valid String or nil but it always returns a valid String. If nil is assigned then the property returns the empty string. The behavior is easy to understand and it does follow the language requirements but it's a little awkward to always have to unwrap the property when getting it, especially when I know it will never be nil. There isn't currently a way to express this behavior in the language.

var text: String { get }
var text: String? { set }

Another use would be something like NSNumber where you can build an immutable object with an Int but read it as a Double, or vice versa. Objective-C uses different names for the properties: intValue, doubleValue etc. We could obviously do that in Swift today but it would be more expressive if we could use this form of type overloading to declare properties.

struct MyValue {
    init(value Int) { self.value = Double(value) }
    init(value Double { self.value = value }
    let value: Double
    let value: Int { return Int(value) }
}
4 Likes

That could be really nice feature just by itself, somewhat similar to the explicit and implicit keywords in C# that allow a whole type to be implicitly/explicitly castable or convertible to another type, but seems more swifty and safe as it is far more constrained (e.g. to a specific usage rather than the whole type across the board)

You bring up a great point.

In the motivating examples from the pitch, the get-only subscript could just as well be get-set and thus allow mutation to work as it does today. The generic set-only subscript would not be involved in mutation.

For example, with a matrix of Bool, we might want to negate a column. As described in the original pitch, suppose Matrix has a subscript that returns a Column and a set-only subscript that accepts any Sequence of the proper length and element type.

Now if we write:

boolMatrix[column: 1].mutateAll{ $0.toggle() }

What should happen?

If the Column-returning subscript is get-set, then the answer is obvious. It works just like it does today. Similarly, once the modify accessor becomes an official part of the language, that subscript could be modify as well.

Indeed, one option we could choose is to say that in-place mutation is a job for the modify accessor, and thus outside the scope of this proposal.

For mutations which keep the type the same, that should be sufficient. And if a mutation converts to a different type, I’m not sure we should want or expect any heroics by compiler.

1 Like

Objective-C allows set-only subscripts, but these are imported as Swift methods.

(Objective-C get-only subscripts, and Objective-C get-set subscripts where the element/index types match, are imported as Swift subscripts.)

If set-only subscripts are added to Swift, I hope there's also a way to use them in the clang importer — without breaking source compatibility.

For example, by importing them as both a Swift subscript and as a deprecated Swift method.

6 Likes

I would like to see this in the language. I think the use-cases given are a little obscure, but one that is perhaps more tangible is to make it easier to conditionally conform to MutableCollection (and other protocol refinements which require inherited subscripts to be settable).

Take a simple Collection wrapper:

struct MyWrapper<Base: Collection>: Collection {
    var base: Base
    struct Index: Equatable, Comparable {
        var wrappedIndex: Base.Index
        static func < (lhs: Self, rhs: Self) -> Bool { lhs.wrappedIndex < rhs.wrappedIndex }
    }
    var startIndex: Index { Index(wrappedIndex: base.startIndex) }
    var endIndex: Index   { Index(wrappedIndex: base.endIndex) }
    func index(after i: Index) -> Index { Index(wrappedIndex: base.index(after: i.wrappedIndex)) }

  // Note: this is a get-only subscript because Base is just a Collection.
  subscript(i: Index) -> Base.Element {
    return base[i.wrappedIndex]
  }
}

extension MyWrapper: MutableCollection where Base: MutableCollection {
  // You can't write this today. This is what I want.
  subscript(i: Index) -> Base.Element {
    set { base[i.wrappedIndex] = newValue }
  }
}

Instead, the way you do this is kind of weird... you just re-implement the entire subscript (getter and all):

extension MyWrapper: MutableCollection where Base: MutableCollection {
  subscript(i: Index) -> Base.Element {
    get { return base[i.wrappedIndex] }
    set { base[i.wrappedIndex] = newValue }
  }
}

This is kind of sub-optimal; it usually means you have to refactor your getter in to a function both subscripts can share. Also, might there be cases where you don't know how the getter is implemented but still want to write a setter? It seems quite unlikely, but if there are, you just wouldn't be able to express them in the language today.

2 Likes

+1 on this feature, I've needed it multiple times in the past