Differences in property mutability with protocol composition vs inheritance

consider the following code:

protocol P {
    var getSetProp: Int { get set }
}

func updateComposition<T: P & AnyObject>(_ object: T) {
    object.getSetProp = 42
 //        `- error: cannot assign to property: 'object' is a 'let' constant

}

protocol Q: AnyObject {
    var getSetProp: Int { get set }
}

func updateInheritance<T: Q>(_ object: T) {
    object.getSetProp = 42 // βœ…
}

why is there an error when setting the property in the protocol composition case, but not in the protocol inheritance case?


edit: seems this was also recently discussed here

1 Like

Because this:

protocol P {
    var getSetProp: Int { get set }
}

is sugar for:

protocol P {
    var getSetProp: Int { get mutating set }
}

Which means it cannot be set on let or function argument bindings.
But adding : AnyObject changes the desugaring to:

protocol Q: AnyObject {
    var getSetProp: Int { get nonmutating set }
}

which can be set on let or function argument bindings.
You can explicitly include mutating or nonmutating if you want the other behavior.

9 Likes

The key thing is that a mutating setter on a non-class constrained protocol can actually fully replace self, which a class cannot. (all the details are in the Masto thread)

2 Likes

Guys, I think you are missing this <T: P & AnyObject> part in OP.

Conceptually, a compiler could judge it legal to promote a mutating set into a nonmutating set when we can tighten the constraint on the target object (from P tp P & AnyObject). I cannot come up with any semantic bad cases (and I'm wrong, see Dante's post).

It's just the current design of protocol composition cannot easily implement this. After OP's code get compiled, there just does not exist a witness symbol in the binary with the semantics of P.getSetProp.nonmutating_set that can be called on object.

As I described in a previous thread on this topic, T: P & AnyObject is insufficient. Consider adding the following code:

protocol MyProto2: P {
  init()
}
extension MyProto2 {
  var getSetProp: Int { 
    get { 0 }
    set { self = init() }
  }
}

final class Foo: MyProto2 {
  init() {}
}

When setting getSetProp on a Foo instance, it actually replaces the instance with a new one, even though Foo satisfies P & AnyObject.

2 Likes

Thanks, you are absolutely right. Allowing calling getSetProp.set on a let Foo will break invariants.