Introduce “Dynamic Member” fulfillment of Protocols

Hi All,

To complement the @dynamicMemberLookup feature I think types with dynamic member lookup should automatically be considered to conform to protocol properties that can be satisfied with a dynamicMember subscript. Both forms should be supported. Aside from the duplicate protocol conformance either case should work (IMO).

protocol PersonProtocol {
    var name: String
    var birthdate: Date
}

@dynamicMemberLookup
struct Person: PersonProtocol {

    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Jon Appleseed", "city": "Cupertino"]
        return properties[member, default: "<no \(member)>"]
    }
    
    subscript(dynamicMember member: String) -> Date {
        let properties = ["birthdate": Date()]
        return properties[member, default: Date()]
    }
}

@dynamicMemberLookup
extension Person: PersonProtocol {}


This would be an additive opt-in feature and not cause any incompatibilities (that I can think of). It would allow dynamic objects to more easily satisfy protocols in a type safe manner and enable Xcode autocomplete and potentially useful compiler hints.

6 Likes

What's the motivation for this? Having the compiler statically check the content of the dynamicMember subscripts seems like it would be either impossible to decide in all cases, or possibly very expensive.
Don't know where I got that idea.

IMO a better alternative would be to declare the type conforms to the protocol, and then have computed properties that do the dynamic behavior. That way it's very explicit that the type conforms.

Can you please share a couple of use cases?

I can’t think of any reasons why I would want this feature.

In my mind, one important aspect of dynamicMembers is that it allows us to keep the syntax of property while providing an alternative store / access strategy. This is especially useful for (NS)Proxy objects -- providing the look-and-feel of a "real" type (the syntactic sugar) while wrapping access control in a general purpose method (the subscript) avoiding the tedium of the boilerplate computed properties.

But as indicated in the docs, when adopting dynamicMembers, the compiler/Xcode can no longer help in constraining autocomplete nor warn us if we misspell a property name. But if we can cast our type w/ dynamicMembers to a real protocol we get that safety back without giving up that advantages above. Being able to constrain access to our object through a well defined protocol on-demand becomes a feature (sorta like an NSProtocolChecker).

Of course I could automate all this with Sourcery to write all the calculated properties (similar to Swift Runtime with Sourcery. Use Sourcery templating to re-create an… | by Jason Jobe | Medium) but it seems like a logical extension to the capability that dynamicMembers is promoting.

2 Likes

see above

FWIW, just explaining thought process here:

This was suggested and I specifically considered this and pushed back on this during the review process. The entire point of the dynamic member lookup feature is to allow unbound syntactic extension of a member lookup in the case when the author of a type cannot enumerate all of the members that a user might want to use statically.

In the case of protocol conformance, a protocol does have a specific static list of members that need to be satisfied. While the @dynamicMemberLookup proposal could be extended to provide implementations of this fixed and static list of members, doing so would be merely be a syntax sugar extension to benefit specific narrow protocol conformances -- it would not benefit general users of the concrete type. It also doesn't (IMO) provide enough benefit to be worth the compiler complexity that it would produce, in terms of generated thunks.

I agree that this is something we "could" do, but as others have asked for, I'd love to see evidence of specific reasons we "should" do this. Those reasons should balance the cost and the benefit of providing such a feature.

-Chris

6 Likes

I'm still trying to fully figure out the right way to do this but I keep circling back around to thinking that protocols can be the type-safe bridge into an objective-c (or such) inspired dynamic runtime environment.

For example, how might we go about building a Swifty Proxy or a RemoteObjectWrapper?

The example could get verbose with strictly boiler plate code very quickly in building different proxies.

@dynamicMemberLookup
class Proxy {
    var wrapped: NSObject
    init (_ w: NSObject) {
        self.wrapped = w
    }

    subscript(dynamicMember member: String) -> String {
        get { return wrapped.value(forKey: member) as? String ?? "" }
        set { wrapped.setValue(newValue, forKey: member) }
    }
    ....
}

// Multiple by many protocols each with lots of vars
class PersonProxy: Proxy, PersonProtocol {}

My understanding is that @dynamicMemberLookup is intended for cases where static declarations are not possible, or are completely impractical. In your bridging scenarios, Objective-C classes rarely have dynamic properties, in the sense that they are not declared in an interface somewhere.

You have also failed to explain why you would need a proxy object in the first place. Objective-C objects are directly accessible, after all, or the proxy wouldn't work, either. What functionality would such a proxy be providing, such that it would also be providing unbounded access to the wrapped object's properties?

In short, I don't understand your use case, and I agree with others that this seems counter to the intent of @dynamicMemberLookup.

1 Like

The most common use case that I know of (and use daily) for NSProxy is mocking Objective-C objects for testing purposes. Basically all libraries (OCMock, OCMockito, etc) rely on NSProxy and the dynamic nature of NSObjects.

I’m not sure that this very narrow use case will improve with this proposal, since it will mean having to change production code to use a slower dynamic model, and the ergonomics are also not really great. I don’t know how feasible it is, but ideally for this use case @testable imports could change static calls for dynamic calls. That way a lot of superfluous protocols that are being created to be able to have testable code can be avoided. Or a @testable mocking solution could be implemented right in the standard library.

I suppose Sourcery and other similar solutions will be the only solution for avoiding writing too much boilerplate. ¯_(ツ)_/¯

Leaving that aside, I think that the most canonical use case for NSProxy may be Remote Procedure Calls, specially for middleware code. Another place where middleware is usually very useful is server code, which I suppose is a more important use case to support in Swift.

Saying that, I’m not sure that this proposal will improve either use case.

1 Like

I think what the OP is getting at, is that there is a specific use case for when one needs dynamic member lookup, but in the target environment, there are several properties/methods that are basically guaranteed to exists, but calling them requires going over the same dynamic member lookup “bridge” so to speak.

For an easy to understand example, say in Objective-C, NSObjectProtocol is implemented in NSObject, so in my hypothetical obj-c dynamic member lookup I can be guaranteed that those members are available, so i’d like the syntax convenience back for those that are known, even if I have to traverse the dynamic bridge to get them.

For all other members, then, it is fully “dynamic” without code completion etc... that’s is until you cast it to another protocol (as an example).

Also, you could hypothetically use a protocol to “guide” the dynamic lookup by specifying a protocol that matches some implementation on the other side, lets say some concrete class, and then using the dynamic lookup if “isKindOf” and if it returns yes, cast it to the protocol etc.

So basically, it lets you “soft type” something that is running in a dynamic environment when you need/want it.

As swift expands to interface with other environments with dynamic languages from swift, it would be convenient for them.

Maybe it’s not so useful, but in any case that’s my interpretation of the OP and generally what I would use it for.

(and apologies if what I describe above is already possible, I haven’t used Dynamic Member Lookup yet)

From my understanding Kotlin in Kotlin.js is providing something along that way: you can have a dynamic type using dynamic keyword (https://kotlinlang.org/docs/reference/dynamic-type.html). Yet in the meantime you can cast to defined external interfaces (Promise - Kotlin Programming Language).

IMO it might come in handy to provide better autocompletion and so a little less runtime error.

Besides, I don't really see how much it is different from a @NSManaged property (no header definition and yet is defined in code)?

I think one situation where this would be nice to have is for wrapped values.

protocol FooProtocol {
  var foo: Int { get }
  ... lot of other attributes
}

@dynamicMemberLookup
struct WrappedFoo: FooProtocol {
  var value: FooProtocol

  subscript<T>(dynamicMember keyPath: KeyPath<FooProtocol, T>) -> T {
    return value[keyPath: keyPath]
  }
  // instead of
  var foo: Int {
    get { return value.foo }
    set { value.foo = newValue }
  }
  ... lot of other getter/setter to type
}
7 Likes

Relatedly, Kotlin supports delegating interface conformance: Interface delegation in Kotlin. Interface delegation is an interesting… | by Giuseppe Villani | Medium

3 Likes

I don't know if this is technically feasible but now that dynamic member lookup has been extended to keypaths this seems reasonable and would avoid tons of getter/setter boilerplate when it comes to model mapping. I just ran into a situation this morning where this would have been much appreciated ;)

2 Likes

I think this proposal needs to be reevaluated again. The major motivation I can think for this is combining composition and polymorphism that can be use as a replacement for inheritance. With dynamicMemberLookup we can already implement composition with dynamic member fulfilment of protocols a lot of boilerplate can be removed.

+1 for re-evaluating this proposal. It would come in really handy for composition and wrapper types.

wrong thread...