Set-only subscripts

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

Well, lessee. That's equivalent to:

{ opt: inout Optional in opt?.mutatingMethod() }(&x[b])

But in the scenario you posit, there's no Optional that you can inout (you can only “out” the Optional). So it would at least require some special rules in the language to describe how this works. Furthermore, to get it to work you'd have to read with one subscript's getter and write back with a different subscript's setter. That adds a lot of language complexity and breaks coherence with modify accessors.

If you want to create an API that allows you to write the semantic equivalent of x[b]?.mutatingMethod() but doesn't allow the semantic equivalent of x[b] = nil, it's easy to write that as a mutating method. Here's how you could add such an API to Dictionary, for example:

extension Dictionary {
  /// Returns the result of calling `body` on the value corresponding to `k`
  /// if `k` is a key in `self`; returns `nil` otherwise.
  @discardableResult
  mutating func mutateValue<Result>(
    ifKeyExists k: Key, body: (inout Value)->Result
  ) -> Result? {
    guard let i = index(forKey: k) else { return nil }
    return body(&values[i])
  }
}
2 Likes

Here's another nice, related utility that maybe should go into the standard library:

extension Optional {
  /// If `self` is non-`nil`, returns `mutate(&w)` where `w` is the wrapped
  /// value of `self`; returns `nil` otherwise.
  mutating func mapInout<R>(_ mutate: (inout Wrapped)->R) -> R? {
    guard var m = self else { return nil }
    self = nil // Maintain uniqueness to avoid CoWs
    defer { self = m }
    return mutate(&m)
  }
}

This allows you to use any API that vends an inout T? to operate on an inout T (if one is available).

7 Likes

It would be great if we could allow mutating associated values directly one day.

switch self {
case .some(inout wrapped):
  mutate(&wrapped)
}

That should also allow avoiding the potential issue with CoW.

Related talk:

3 Likes

I have a use case that would benefit from set-only subscripts. I've created a JSONObject type, which uses string-based dynamic member lookup to store values of a JSONValue enum type.

enum JSONValue: Codable {
   case string(String)
   case number(Double)
   // etc...
}

@dynamicMemberLookup
struct JSONObject {
   private var dict: [String: JSONValue]

   public subscript(dynamicMember key: String) -> JSONValue? {
      get { return dict[key] }
      set { dict[key] = newValue }
   }

   // ...
}

I've implemented the various ExpressibleBy*Literal protocols for JSONValue, which means that you can construct JSON object like this:

var obj = JSONObject()
obj.name = "Fred Weasley"
obj.age = 17

However, I cannot use variables in the same way, because they're not literals.

let food = "Pizza"

// Error: Cannot assign value of type 'String' to type 'JSONValue'
obj.favoriteFood = food

This disparity between literals and variables is an ergonomic burden on the type. I would love to be able to implement set-only implementations of subscript(dynamicMember:) for the various supported JSONValue types, but the language does not support that. I could implement a separate setValue(_:forKey:) method, but again, the ergonomic distinction between literals and variables makes this non-ideal.

Anyway, that's my use case.

4 Likes

[Cross-reference] related issues were covered in

1 Like

Bumped into this in a fairly interesting case as well. set-only subscripts would really help us build more correct APIs for tracing as well.

In Swift Tracing we have a Span type that has some attributes, those have some predefined (by various standards, or just your own team or company) keys/types you should set. It is generally not recommended to allow reading those values as that might cause Span "abuse" -- some specifications like Open Telemetry even say that attributes MUST be write-only.

We implement attributes by:

extension SpanAttributes {
    /// Enables for type-safe fluent accessors for attributes.
    public subscript<T>(dynamicMember dynamicMember: KeyPath<SpanAttribute, SpanAttributeKey<T>>) -> SpanAttribute? { ... }
}

which enables amazing APIs like:

import TracingOpenTelemetrySupport
// imports semantic conventions for the attributes, 
// making the following available:

span.attributes.http.statusCode = 200

without the import the .http.statusCode does not exist; This is excellent because teams can pick "which semantics we're using" and only those appear on attributes.

At the same time otel strongly suggests (well "MUST"...) that attributes must not be readable. They should be set-only. We cannot express this because we use subscripts to implement the dynamic memberlookup dance. We'll violate the spec a little bit here, admittably the nice API is worth it here, but worth pointing out.

Just another datapoint where set-only would have been useful.

4 Likes

huge +1 from me, provided the compiler can figure out that equally named subscripts where one is get and the other is set can receive mutating method calls:

enum MyEnum {
   case first(Int)
   case second(String)
   
   struct First{}
   
   subscript(first: First) -> Int?{
      guard case .first(let first) = self else {return nil}
      return first
   }

   subscript(first: First) -> Int{
      set{
          self = .first(newValue)
      }
   }
   
}

var myValue : MyEnum = .first(42)
myValue[First()] += 1 //43, since value can be read and then set
myValue = .second("Hello, World")
myValue[First()] += 1 //still "Hello World", since value could not be read

Ideally, there would be additional KeyPaths for exactly this use-case that would be auto-synthesized for enums so we finally have native prisms (see Lenses and Prisms in Swift: a pragmatic approach | Fun iOS) without boilerplate code or Sourcery.

The requirements so the compiler oks mutating methods would be: 1. the get/set properties/subscripts need to be equally named and 2. the set-only-property needs to have a type where the compiler knows how to a) safely cast the value to the get-only-type and b) failably downcast to the set-only-type. That is, optionals would work, class and subclass would work, protocol existentials and conforming types would work, and Any and anything would work. A further direction of development could be to provide some public API to make more pairs of types work.

Finally, one more thought: I've long hoped to see some public "mutate" API:


var something = 42

var myProperty : Int {
get {
something
}
mutate {
change(&something) //change is (inout Int) -> Void
}
}

This would sometimes be helpful in order to avoid triggering copy-on-write or other undesirable effects. Here, we could explicitly encode the downcast:

struct Foo {
   
   //never nil, but optional so we can prevent potential copy-on-write
   private var _something : MyType!

   var something : MyType {
      _something
   }

   var something : MyOtherType {
      mutate {
         guard var cast = myDowncastLogic(_something) else { return }
         _something = nil //prevent copy-on-write
         change(&cast)
         _something = mySafeCastLogic(cast)
      }
   }

}

Edit: Even though it is possible to generate a setter from a "mutate", the above also shows that having a mutate should not prevent us from writing a setter. While mutate may be more efficient and should generally be preferred when we call mutating methods, there are legit cases where setting the value should be possible even if reading the value fails.

Caveat: if this feature was implemented, it would allow for some code that may be considered abusive. You may be able to call mutating methods of seemingly unrelated types. Also, you may have properties where the getter is optional, but the setter isn't, so you can omit the "?" when calling a mutating method. Maybe, we should introduce also a mutate? that has the same signature but forces the caller to use a "?" before calling mutating methods (except infix methods!!), but that may be seen as ill-typed it is entirely up to the programmer implementing mutate or mutate? which one he writes (except the compiler forces you to use the change closure in mutate). The compiler-generated inout API when you only provide a setter of a subtype would be mutate? my default then. However, this still allows for quite odd-looking code.

Generally, I'd argue that the possibility of awkward or unsafe APIs is not a high price to pay if at the same time efficient, elegant and safe features are enabled that weren't possible before. The awkward and unsafe APIs will then just go the way of the dinosaurs and it's up to the community to promote clean code. After all, the claim that in Swift you can't have null-pointer-exceptions is a hoax - we do have ! and we can hide an optional behind a computed property just like @Environment does. But it is made sufficiently complicated that most people just naturally avoid doing this unless it's really really legit.

Apologies for reviving an old thread. I hope the following is helpful. You can use availability conditions and @_disfavoredOverload to make the original motivating example compile and safe to use:

var a = nums[2...4]
a = nums[2...4]  // this now compiles!
let b: ArraySlice<Int> = nums[2...4]  // this too!

extension MutableCollection {
    @_disfavoredOverload  // <----
    subscript<S: Sequence>(_ range: Range<Index>) -> S
    where S.Element == Element
    {
        @available(*, unavailable)  // <----
        get { fatalError() }
        set { ... }
    }
    
    @_disfavoredOverload  // <----
    subscript<R: RangeExpression, S: Sequence>(_ range: R) -> S
    where R.Bound == Index, S.Element == Element
    {
        @available(*, unavailable)  // <----
        get { fatalError() }
        set { ... }
    }
}

This also works when the get accessor uses Optional but set shouldn't:

struct Foo {
    var bar = 0
}

var myOptional = MyOptional.some(Foo())
myOptional.bar
myOptional.bar.magnitude  // Getter for 'subscript(dynamicMember:)' is unavailable: Value of optional type must be unwrapped...
myOptional.bar?.magnitude
myOptional.bar = nil      // 'nil' cannot be assigned to type 'Int'
myOptional.bar = 42
MyOptional Implementation
@dynamicMemberLookup
enum MyOptional<Wrapped> {
    case none
    case some(Wrapped)
    
    subscript<T>(dynamicMember member: KeyPath<Wrapped, T>) -> T? {
        switch self {
        case .none:
            nil
        case .some(let wrapped):
            wrapped[keyPath: member]
        }
    }
    
    @_disfavoredOverload
    subscript<T>(dynamicMember member: WritableKeyPath<Wrapped, T>) -> T {
        @available(*, unavailable, message: "Value of optional type must be unwrapped to refer to member of wrapped base type")
        get { fatalError() }
        set {
            if case .some(var wrapped) = self {
                wrapped[keyPath: member] = newValue
                self = .some(wrapped)
            }
        }
    }
}
3 Likes