Modifications with keyPaths in protocol extension

Basically my idea was to have like generic Builder protocol. I wanted to have pre implemented set method with keyPath. For some reason it doesn't compile with error message: "Cannot assign through subscript: 'self' is immutable" even though extension is only for AnyObject so self should be mutable. Side note: I also tried version without casting to ReferenceWritableKeyPath with same error. Is anyone have idea how I can make this work or explanation why it's not working?

protocol Builder {
    associatedtype Result
    func set<T>(_ keyPath: WritableKeyPath<Self, T>, to value: T) -> Self
    func build() -> Result
}

extension Builder where Self: AnyObject {
    func set<T>(_ keyPath: WritableKeyPath<Self, T>, to value: T) -> Self {
        if let path = keyPath as? ReferenceWritableKeyPath<Self, T> {
            self[keyPath: keyPath] = value
        }
        return self
    }
}

Are you using this protocol on a class or a struct? I was tol WritableKeyPath works on a struct, and ReferenceWritableKeyPath works on a class.

1 Like

Playing around with code derived from your code above, putting in this line of code shown below as the very first line of the extension gets rid of that error saying that self is immutable.

var `self`: Self = self as! Self

This shadows self, and doesn't mutate self at all, but rather a shadowed local variable.

1 Like

Thanks for the replays @Shinehah-Gnolaum and @sveinhal. Builder is gone be a class, because it usually mutates during it's lifetime. Overall I found a solution with ReferenceWritableKeyPath that really suits my needs. Haven't used it in prod, but tested on some examples. Here it is:

protocol Builder: AnyObject {
    associatedtype Result
    func set<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, to value: T) -> Self
    func build() -> Result
}

extension Builder {
    func set<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, to value: T) -> Self {
        self[keyPath: keyPath] = value
        return self
    }
}

struct Person {
    let name: String
    let age: Int
    let adress: String
}

final class PersonBuilder: Builder {
    var name: String = ""
    var age: Int = 0
    var adress: String = ""

    func build() -> Person {
        Person(name: name, age: age, adress: adress)
    }
}

PersonBuilder()
    .set(\.name, to: "Jhon")
    .set(\.adress, to: "street 1")
    .set(\.age, to: 25)
    .build()

I think your original code would work with this typo fixed.

2 Likes

You're right! Thanks