-1

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?

hopy
  • 559
  • 3
  • 18
  • 1
    What are you trying to achieve? You can just access the person property and assign it to a mutatble variable. Or you can assign the output of the method to a mutatble variable. `group().name` is meaningless - you can't call a struct!. `group.name = "Wong"` would work. – flanker Jan 28 '22 at 02:00
  • 1
    @flanker “`group().name` is meaningless - you can't call a struct!” - It is not meaningless. Per [SE-0253](https://github.com/apple/swift-evolution/blob/master/proposals/0253-callable.md), `callAsFunction` does precisely that. His problem is that he’s dealing with value types, so returning a struct and attempting to change one of its properties in this way doesn't make any sense. He could make `Person` a reference type, or just use another pattern altogether. But `group()`, itself, is syntactically correct and just calls his `callAsFunction`. – Rob Jan 28 '22 at 05:50
  • 1
    @flanker - “`group.name = "Wong"` would work.” - No it wouldn't. `group.person.name = "Wong"` would, though. That defeats his purpose, which is likely to explore and understand [SE-0253](https://github.com/apple/swift-evolution/blob/master/proposals/0253-callable.md), [SE-0216](https://github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md), and [SE-0195](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md). Not sure if this is a good use-case, but I appreciate hopy’s attempt to grok what’s going on here. – Rob Jan 28 '22 at 05:58
  • see my updated above please ;) – hopy Jan 28 '22 at 06:37
  • use reference type instead – Quang Hà Jan 28 '22 at 06:45
  • Thanks @Rob for the context. Some context or reference to the SE proposal in the question to indicate it wasn’t referring to a current language feature would have helped. And yeah, I meant .name = … :) – flanker Jan 28 '22 at 10:18
  • Actually, these have all been adopted. They are current language features. I just referred back to the original proposals as that is the clearest official description and provide useful background on the motivations behind these enhancements. – Rob Jan 28 '22 at 17:50
  • Maybe the `Group`/`Person` is just a poor simplified example, but this really doesn't make sense to me. I write Ruby as a day job, so I'd say I have a pretty decent grasp of it, and I don't see why one would want to use message forward here. – Alexander Jan 28 '22 at 20:25

2 Answers2

2

You're using the wrong dynamic method. What you want is dynamicMemberLookup. Watch closely. First, the preparation:

struct Person {
    var name: String
}

@dynamicMemberLookup
struct Group {
    var person: Person
    subscript(dynamicMember kp:WritableKeyPath<Person,String>) -> String {
        get { self.person[keyPath:kp] }
        set { self.person[keyPath:kp] = newValue }
    }
}

Now look at what that allows you to say:

var group = Group(person: Person(name: "James"))
group.name = "Wong"
print(group.person) // Person(name: "Wong")

Do you see? We set the name of the Group even though it has no name property, and the result was that we set the name of the Group's person which does have a name property.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • see my updated above please ;) – hopy Jan 28 '22 at 06:37
  • 1
    Yes, but that doesn't change my answer. dynamicMemberLookup is intended precisely to be as close as Swift comes to Ruby-like forwarding. – matt Jan 28 '22 at 06:52
1

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)
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks! in your example 'name' and 'city' in Person is all String type,but there are 100 different return type in my case,so I still have to write 100 subscript methods ;) – hopy Jan 31 '22 at 01:00
  • No, just make it generic. You only need one subscript method. – Rob Jan 31 '22 at 01:09
  • Yes, use generic worked for properties! but how do I invoke method in Person from Group? eg: there is method "working' in Person, how do this: group.working() – hopy Jan 31 '22 at 02:37
  • I tryed @dynamicCallable and callAsFunction, but only can invoke working method like this: group().working() – hopy Jan 31 '22 at 02:38
  • Not that I know of. I know that [SE-0216 - Future Directions](https://github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md#future-directions) contemplates `@dynamicMemberCallable` in some future release, so maybe that will do what you want in the future. – Rob Feb 01 '22 at 01:32