[swift-evolution] [Pitch] Introduce User-defined "Dynamic Member Lookup" Types

Paul Cantrell cantrell at pobox.com
Fri Nov 17 21:45:51 CST 2017


> On Nov 17, 2017, at 12:08 AM, Brent Royal-Gordon <brent at architechies.com> wrote:
> 
>> On Nov 16, 2017, at 1:44 PM, Paul Cantrell <paul at bustoutsolutions.com <mailto:paul at bustoutsolutions.com>> wrote:
>> 
>> In the example you bring up:
>> 
>>> you can write `def someMember` and `def someMember= (newValue)`)
>> 
>> …there is no overloading. The = is _part of the method name_, i.e. there is a `someMember` method and a `someMember=` method.
> 
> You're right—I was speaking imprecisely when I used the word "overloading". Nevertheless, Ruby doesn't quite directly interpret `x.someMember = y` as `x.someMember= (y)`—it supports operators like `+=`, which do a getter-operation-setter dance.

True, `foo.bar += x` is just sugar for `foo.bar = foo.bar + x`, which in turn is sugar for `foo.bar=(foo.bar.+(y))`. But does this pose a problem for a Swift → Ruby bridge? Presumably such a bridge would:

1. define a Swift += operator on RubyObj that does the same expansion as above, and

2. forward the property setter for `bar` to Ruby as a `bar=` invocation.

I don’t think there’s a problem here. But maybe I’m missing it?

> 
>> The following are equivalent:
>> 
>>     foo.bar = 3  # just sugar
>>     foo.bar=(3)
>>     foo.send("bar=", 3)
>> 
>> Ruby allows ?, !, and = as the last char of method names, and AFAIK other than the special sugar around setters, they are just parts of the method name with no further semantic significance.
> 
> You're correct that, with this design, you could access Ruby accessors from Swift with syntax like:
> 
> 	myObj.name()
> 	myObj.`name=`("Chris")		// If we loosened the characters allowed in backticks
> 
> My point is simply that this is a poor mapping, for much the same reason `dog["add_trick"].call(…)` is a poor mapping. It's technically correct and exposes the functionality, but it's awkward and doesn't match the user's mental model.

Well sure, that would be awkward, but doesn’t Chris’s proposed protocol allow idiomatic access to work too?

    extension RubyObj: DynamicMemberLookupProtocol {
      subscript(dynamicMember member: String) -> RubyObj {
        get {
          return /* …some deferred callable thing… */
        }
        set {
          RubyObject_send(rubyObject, method: "\(member)=", args: [newValue])
        }
      }
    }

That would make `myObj.name = “Chris"` in Swift work as expected. (This is using a made-up RubyObj binding, but I think it makes the point.)

What _is_ a bit nasty, as you pointed out, is that `myObj.name` does _not_ work as expected. Instead of returning the name, it returns a closure that returns the name when called. Yuck.

In Ruby, `myObj.name()` is equivalent to `myObj.name`, and either works. In Swift, I don’t see that it’s possible to make both work with Chris’s proposal.

> If we had separate subscripts for methods and properties, then the property subscript could immediately call the appropriate getters and setters, while the method subscript could return a ready-to-call `Method` object.

Hmm, yes, I think you’re right. It seems like that would fix the nastiness above. Better yet, why bother with the ready-to-call Method-like object? Just call it! A Ruby binding with separate property and method handling would then look like this:

    extension RubyObj: DynamicMemberLookupProtocol {
      func callDynamicMethod(dynamicMethod method: String, args: [RubyObject]) -> RubyObj {
        get {
          return RubyObject_send(rubyObject, method: member, args: args)
        }
      }
      
      subscript(dynamicMember member: String) -> RubyObj {
        get {
          return RubyObject_send(rubyObject, method: member, args: [])
        }
        set {
          RubyObject_send(rubyObject, method: "\(member)=", args: [newValue])
        }
      }
    }

When Swift sees myObj.name, it uses the getter subscript. When Swift sees myObj.name(), it uses the method invocation. Both work in Swift just as they do in Ruby.

Note that for this to work, the Swift compiler itself has to tell the Ruby binding whether the member access looks like a property or method access.

• • •

I confess I didn’t follow every detail of your “wall of text,” but I did find this example compelling:

> That would work for arbitrary fixed sets of properties, but we can extend this to wrapper types. Imagine you want to write the `UIAppearance` class I mentioned previously. (Actually, we'll call it `UIAppearanceProxy` to avoid names already used in UIKit.) Your basic structure looks like this:
> 
> 	class UIAppearanceProxy<View: UIAppearance> {
> 		let containers: [UIAppearanceContainer.Type]
> 		let traits: UITraitCollection
> 		
> 		var properties: [PartialKeyPath<View>: Any] = [:]
> 	}
> 
> Now, to make all properties of the `View` class settable on this class, you can overload the `additionalProperty` subscript to accept `View` keypaths:
> 
> 	extension UIAppearanceProxy {
> 		subscript<T>(additionalProperty viewKeyPath: KeyPath<View, T>) -> T? {
> 			get {
> 				return properties[viewKeyPath] as! T?
> 			}
> 			set {
> 				properties[viewKeyPath] = newValue
> 			}
> 		}
> 	}


That’s an impressive level of dynamism for a statically type-checked idea. Awful lot of complexity to bite off, though!

Cheers,

Paul

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171117/e27d76d5/attachment.html>


More information about the swift-evolution mailing list