Search code examples
swiftmethodsimmutabilitymutable

Is there any way to make the method return a mutable value?


as shown in the code below:

struct Person {
    var name: String
}

struct Group {
    var person: Person
    
    func callAsFunction() -> Person {
        // Person is immutable value
        person
    }
}

var james = Person(name: "James")
var group = Group(person: james)
group().name = "Wong" //ERROR: Cannot assign to property: function call returns immutable value

group() return an immutable value, that can't be changed! So Is there any way to make the callAsFunction() method return a mutable value?

Thanks ;)


Updated:

My idea is to transfer all the calls and visits of the Group to the Person object in the Group, just like using Person directly.

I can't use dynamicMemberLookup because I don't know what method or property there will be in Person. For example, there may be 100 methods and properties in Person (not only one name property as demonstrated), and it is impossible for me to write 100 subscript methods with dynamicMemberLookup.

My needs are a bit like proxy objects in the Ruby language. Accessing an object (Group) actually accesses another object (Person) inside it, as if the Group does not exist.

ruby proxy patterns: https://refactoring.guru/design-patterns/proxy/ruby/example

CallAsFunction is the closest implementation so far, but requires that Person cannot be a Struct, otherwise it cannot be assigned to its properties.

Maybe it's not possible to implement this feature in Swift yet?


Solution

  • The callAsFunction simply returns (a copy of the) Person, which is a value type. You cannot then mutate the property of it like that. It is equivalent to the following:

    struct Person {
        var name: String
    }
    
    Person(name: "Foo").name = "Bar"
    

    That returns the same error:

    enter image description here

    If Person was a reference type, it would have worked, but not for a value type. And even if you took your value type, and first assigned it to a variable before mutating it, you would only be mutating your copy, not the original.

    If you want the behavior you want, you would use a @dynamicMemberLookup as suggested by matt (+1) and outlined in SE-0195.


    You said:

    I can't use dynamicMemberLookup because I don't know what method or property there will be in Person. For example, there may be 100 methods and properties in Person (not only one name property as demonstrated), and it is impossible for me to write 100 subscript methods with dynamicMemberLookup.

    You do not need “100 subscript methods.” It is the motivating idea behind @dynamicMemberLookup, namely that the properties will be determined dynamically. E.g., here is Person with two properties, but Group only has the one @dynamicMemberLookup.

    struct Person {
        var name: String
        var city: String
    }
    
    @dynamicMemberLookup
    struct Group {
        var person: Person
        subscript(dynamicMember keyPath: WritableKeyPath<Person, String>) -> String {
            get { person[keyPath: keyPath] }
            set { person[keyPath: keyPath] = newValue }
        }
    }
    
    var group = Group(person: Person(name: "James", city: "New York"))
    group.name = "Wong"
    group.city = "Los Angeles"
    print(group.person) // Person(name: "Wong", city: "Los Angeles")
    

    If you want to handle different types, make it generic:

    struct Person {
        var name: String
        var city: String
        var age: Int
    }
    
    @dynamicMemberLookup
    struct Group {
        var person: Person
        subscript<T>(dynamicMember keyPath: WritableKeyPath<Person, T>) -> T {
            get { person[keyPath: keyPath] }
            set { person[keyPath: keyPath] = newValue }
        }
    }
    

    And

    var group = Group(person: Person(name: "James", city: "New York", age: 41))
    group.name = "Wong"
    group.city = "Los Angeles"
    group.age = 42
    print(group.person) // Person(name: "Wong", city: "Los Angeles", age: 42)