Set-only subscripts

big +1!

Just read through the whole thread. Looks like the feedback was overall very positiv. I really like the idea as well.

I have a similar wrapper type which I can access via dynamic member lookup. A dedicated subscript for setting would be awesome.

subscript<Y>(dynamicMember dynamicMember: KeyPath<T, Y>) -> Proxy<Y> {
   get { ... }
}

subscript<Y>(dynamicMember dynamicMember: WriteableKeyPath<T, Y>) -> Y {
   set { ... }
}

@Nevin Are you planing to push this into a official proposal?

2 Likes

I just came across another use-case for set-only properties.

I wanted to make the floating-point properties sign, exponent, and significand be settable.

So I wrote code like this:

extension FloatingPoint {
  var exponent: Exponent {
    get { exponent }
    set { self = Self(sign: sign, exponent: newValue, significand: significand) }
  }
}

Now this does in fact work, and the getter even properly accesses the protocol requirement.

The problem is, when writing a new type that conforms to FloatingPoint, the compiler thinks the extension provides a default implementation for the protocol requirement. Which means the type would actually compile without providing its own getter for exponent, and then at runtime accessing that property would cause an infinite loop.

Whereas if I could write a set-only property:

extension FloatingPoint {
  var exponent: Exponent {
    set { self = Self(sign: sign, exponent: newValue, significand: significand) }
  }
}

Then it would not appear to be a default implementation of the protocol requirement.

I’d love to, but I don’t know how to write an implementation for it.

6 Likes

I could probably help with the implementation side. To clarify, is this proposal for set-only subscripts or for both subscripts and properties?

1 Like

That would be great!

I think both properties and subscripts should be included. They’re both useful, and keeping them consistent is valuable too.

Sounds good! Once you have a draft proposal (or basically just something that explains how this feature works), let me know and I’ll take a look at implementing it!

1 Like

I'm not sure this is a good idea. Semantically, a set-only subscript or property is just a mutating method, so this amounts to a different syntax for writing a mutating method. By expanding the constraints on subscripts and properties, we weaken their meaning. For example, seeing x[b] = y in valid code no longer means you can write x[b].mutatingMethod(). So it's not obvious to me that this change improves the language overall. I can see that it increases symmetry (between get and set) but that is a benefit to the writer that potentially hurts the reader.

14 Likes

Seems like a few of the examples here amount to wanting the getter to return a Whatever? without allowing you to set the value to nil.

Do you think that’s a bad idea? If not, how would you characterize the difference between it and the example you gave of what you don’t think is a good idea?

Yeah, I think it's a bad idea. It has essentially the same problem: it weakens the meaning of subscripts and properties by expanding the apparent relationships between setters and getter. Again you wouldn't be able to write x[b].mutatingMethod() or x[b]?.mutatingMethod()

1 Like

As a non-controversial first step, what about if we only allow writing set-only properties/subscripts when there is a corresponding getter available (basically, just allowing the two halves to be broken up)?

That would solve both the conditional conformance to MutableCollection scenario, and @Nevin’s floating point exponent scenario, without disrupting existing subscript semantics.

But really, I’m not sure there is such a conceptual difference. The availability of mutating methods today also depends on the presence of a setter and it isn’t always clear at the point of use whether that exists. Especially for subscripts - users could already assume that all subscripts have setters (otherwise, it would be a property or function. It only needs in-out access if it can mutate).

This feature would just generalise that to say that the property/subscript must have both a getter and setter.

Do you have a concrete example where that increases expressivity, rather than just code organization?

I admit, I find it frustrating that I have to switch from my usual practice of declaring each level of conformance a protocol at a time when defining a MutableCollection, but I have yet to hit a time when this was materially limiting.

"This is already a problem, let's generalize that problem" doesn't seem very persuasive to me.

2 Likes

No, but that suggestion is just intended to tackle the non-controversial parts. It’s still a nice thing to have if we can do it.

I mean it more like “this is already a fact of life”. Even if we want users to be able to assume certain things, the reality is that working with properties/subscripts already has certain levels of complexity. This doesn’t substantially add to it, IMO.

One wrinkle that did just occur to me though is when developers use erased properties and subscripts - we always assume that you can read from a KeyPath :grimacing:

1 Like

I believe that @Karl's minimal suggestion would also allow this example, for which it would increase expressivity relative to current Swift:

Wait, I’m confused. Why wouldn’t x[b]?.mutatingMethod() work?

I'm not sure that's a good goal. We already have a mechanism for doing this e.g. the replaceSubrange method (though we don't have such a method on non-RRC collections, but could). Why is it important to facilitate that operation through subscript assignment?

The purpose of get/set properties and subscripts is to allow them to behave to the user like l-values that can be mutated "in-place"*, as if you were mutating a regular stored property. This means you can, for example, write swap(&a[i], &b[j]).

But what's proposed is not doing that. It's just providing a new way to write mutating methods using subscript syntax. I agree with Dave that this weakens the meaning of subscripts.

*in use, if not in practice, pending language enhancements and elusive solutions)

3 Likes

You cannot mutate that which you cannot read

But you can read it. Maybe I didn’t communicate it clearly, but the context is a situation where the getter must exist. It’s just that if the getter returns a T?, the setter can take a non-optional T.

I see. You’re suggesting something that would be very confusing to me.

It’s come up a few times for situations where the data might not exist, but you don’t want allow explicitly setting the value to nil. I think “returning nil instead of crashing if you try subscript an array with an invalid index” was the first use-case I remember seeing. (I’m not saying this is or isn’t the best way to do that, just explaining a motivating example that I remember seeing.)

The big question is, what is the type of the entire subscript.

subscript(...) {
  get -> Get { ... }
  set -> Set { ... }
}

It may make sense in some scenarios if Get is a subtype of Set or vice-versa. However, I could not write foo(&x[b]) regardless of whether it is foo(_: inout Get) or foo(_: inout Set), not even with a common supertype or a common subtype (if we can even somehow have one). So it already can not do what normal l-value can, on a syntax that, as @Ben_Cohen said, suppose to behave like l-values. We may be able to expand the type system to accommodate this, but that's a huge undertaking for something of this size.

Also x[b]?.mutate() is (syntactically) just a sugar for

if var tmp = x[b] {
  tmp.mutate()
  x[b] = tmp
}

At best, we could say it should work with Getter == Optional<Setter>, but that does not apply to general syntax x[b].mutate(). And I do question its utility if we only limit the feature to extra Optional on the getter side.

3 Likes