2

I'm working on a method I can use to only update a property with a new value if the new value is different from the current. I have come up with a protocol for this. However, I am getting compiler errors:

Here are a few sample models:

class Model {
    var id: Int = 0
}

class Person: Model {
    var name: String = ""
}

class Dog: Model {
    var color: UIColor = .black
}

Here is the protocol:

protocol Updatable: AnyObject{

    @discardableResult
    func update<Value: Equatable>(
        _ keyPath: ReferenceWritableKeyPath<Self, Value>,
        to value: Value
    ) -> Bool
}

And here is a default implementation:

extension Updatable {
    @discardableResult
    func update<Value: Equatable>(
        _ keyPath: ReferenceWritableKeyPath<Self, Value>,
        to value: Value
    ) -> Bool {
        guard self[keyPath: keyPath] != value else { return false }
        self[keyPath: keyPath] = value
        return true
    }
}

Now I would like for all of my Model subclasses to inherit this update feature:

extension Model: Updatable {}

However, I get a compiler error:

Protocol 'Updatable' requirement 'update(_:to:)' cannot be satisfied by a non-final class ('Model') because it uses 'Self' in a non-parameter, non-result type position

Now I understand why this would be a problem due to inheritance. The Self constraint would be different for each Model subclass and overriding would be impossible.

What gets me is that I can work around this issue by defining a blank protocol and adding a default protocol method with the same signature to an extension:

protocol Updatable2: AnyObject { }

extension Updatable2 {

    @discardableResult
    func update2<Value: Equatable>(
        _ keyPath: ReferenceWritableKeyPath<Self, Value>,
        to value: Value
    ) -> Bool {
        print("Default: \(keyPath) -> \(value)")
        guard self[keyPath: keyPath] != value else { return false }
        self[keyPath: keyPath] = value
        return true
    }
}


extension Model: Updatable2 {}

The compiler gives me no trouble at all. All of the following are valid:

let model = Model()
model.update2(\.id, to: 200)

let person = Person()
person.update2(\.name, to: "Sally")
person.update2(\.id, to: 400)

let car = Car()
car.update2(\.wheels, to: 5)

let models: [Model] = [
    model,
    person,
    car,
]
models.forEach { $0.update2(\.id, to: 666) }

How is this possible?

Rob C
  • 4,877
  • 1
  • 11
  • 24
  • Just to be clear, does your workaround involve deleting the old `Updatable`? – Sweeper Aug 07 '22 at 10:59
  • Oh, I should have made that clear. I have no need for both protocols. So yeah, only one protocol would exist. – Rob C Aug 07 '22 at 11:11
  • I think the real issue here is that you, @RobC, have not been able to shake the inner _intuitive_ conviction that `MyType` and `MyType` should be somehow related just because Person descends from Model. But of course this is not true; as you know with your _rational_ brain, they are completely independent types. There is no polymorphism here, no inheritance. Well, the same for `MyType`. Self here is not polymorphic. It does not mean "Me and my descendants". It just means Me. – matt Aug 07 '22 at 12:17

1 Answers1

2

Now I understand why this would be a problem due to inheritance. The Self constraint would be different for each Model subclass and overriding would be impossible.

Overriding is not the reason. The reason, as explained in this question, is that the inherited method does not implement the protocol anymore.

class ModelSubclass: Model {
    /*
     inherits
     func update<Value: Equatable>(
         _ keyPath: ReferenceWritableKeyPath<Model, Value>,
         to value: Value
     ) -> Bool {
     
     but should have
     func update<Value: Equatable>(
         _ keyPath: ReferenceWritableKeyPath<ModelSubclass, Value>,
         to value: Value
     ) -> Bool {
     */
}

Naturally, removing the requirement works, because this whole thing is caused by the protocol requiring ModelSubclass to have a certain method.

Now, I know what you're going to say: "But I have a default implementation, so every implementation should have all the update methods it needs!" Well, logically I think you are correct, but the Swift compiler doesn't bother checking for things like that. This is the line of code in the compiler where it emits the error message. As you can see, before that, it only checks if the protocol has Self in an invariant position (the final check is done before calling the enclosing method):

if (selfRefInfo.selfRef == TypePosition::Invariant) {
  // References to Self in a position where subclasses cannot do
  // the right thing. Complain if the adoptee is a non-final
  // class.

Side note:

If Self is the parameter type (contravariant) or return type (covariant), then it's fine. Consider:

protocol Foo {
    func f(x: Self)
    func g() -> Self
}

class Bar: Foo {
    func f(x: Bar) { }
    
    // note that the covariant Self return type is required here
    // you cannot use Bar here
    func g() -> Self {
        fatalError()
    }
}

class BarSubclass: Bar {
    /*
     inherits:
     func f(x: Bar)
     
     which can take any BarSubclass as parameter, so it can satisfy
     func f(x: BarSubclass)
     
     also inherits:
     func g() -> Self
     
     which is the same as what it should implement :)
     */
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313