One object providing dynamicMemberLookup for a multiple other objects — possible?

I'd like to construct a proxy object that's capable of mirroring the properties of multiple other objects.

Here's an example of what I mean:

protocol Linked {
    
    associatedtype Next = Never
    
}

struct A: Linked {
    
    let propertyA = 0
    
    typealias Next = B
    
}

struct B: Linked {
    
    let propertyB = 1
    
    typealias Next = C
    
}

struct C: Linked {
    
    let propertyC = 2
    
}

Ideally, I could introduce another object, Proxy, pass it the type of the first object in the list, and create an interface based on those objects:

let proxy = Proxy<A>()

proxy.propertyA // 1
proxy.propertyB // 2
proxy.propertyC // 2

My first intuition was:

@dynamicMemberLookup
struct Proxy<LinkedType: Linked> {
    
    subscript<T>(dynamicMember keyPath: KeyPath<LinkedType, T>) -> Void { () }
    
    subscript<T>(dynamicMember keyPath: KeyPath<Proxy<LinkedType.Next>, T>) -> Void { () }
    
}

Which works, except there's no autocomplete available for any of the properties I reference at all, which is terrible for developer experience.

This made me wonder if I was doing something wrong with that implementation, so I played with a different one:

@dynamicMemberLookup
struct Composition<Current, Next> {
    
    subscript<T>(dynamicMember keyPath: KeyPath<Current, T>) -> Int { 0 }
    
    subscript<T>(dynamicMember keyPath: KeyPath<Next, T>) -> Int { 0 }
    
}

let composition = Composition<A, Composition<B, C>>()

Which autocompletes without issue — propertyA, propertyB, and propertyC all show up.

All that said:

  • Is there any reason why the first implementation shouldn't autocomplete, or is it a bug?
  • Is it possible to take advantage of the second implementation and the autocomplete that comes with it, without having to manually construct the Composition list (and allowing for an arbitrary number of linked objects)?

I think the reason it doesn't work is because there is no guarantee Next also conforms to Linked

I'm not sure if this is a good idea, but this would probably work:

protocol Linked {
  associatedtype Next: Linked = Tail
}

struct Tail: Linked {}

You could also do extension Never: Linked {} but that may add unwanted dynamic members to your proxy.

You're exactly right! Thank you!

Here's a follow up scenario that I think should work, but also seems to break autocomplete. My thinking is that the where condition on Proxy should fix the previous issue (no guarantee of conformance) that I ran into before, but it doesn't:

protocol Node { }
protocol LinkedNode: Node {

    associatedtype Next: Node

}

@dynamicMemberLookup
struct Proxy<CurrentNode: Node> {
    
    subscript<T>(dynamicMember keyPath: KeyPath<CurrentNode, T>) -> Int { 0 }
    
}

extension Proxy where CurrentNode: LinkedNode {
    
    subscript<T>(dynamicMember keyPath: KeyPath<Proxy<CurrentNode.Next>, T>) -> Int { 0 }
    
}

struct A: LinkedNode { typealias Next = B; let propertyA = 0 }
struct B: Node { let propertyB = 1 }

let proxy = Proxy<B>()

proxy.propertyB // works — but no autocomplete on `proxy`