How do I use dynamic member lookup in a generic property wrapper to return the looked-up member wrapped by the same property wrapper?

Sorry the title is a mouthful.

Basically this is what I'm trying to do:

@propertyWrapper @dynamicMemberLookup
struct Wrapper<Wrapped> {
	var wrappedValue: Wrapped
	
	subscript<Member>(
		dynamicMember keyPath: WritableKeyPath<Wrapped, Member>
	) -> Wrapper<Member> {
		get {
			Wrapper(wrappedValue: wrappedValue[keyPath: keyPath])
			// error: cannot convert return expression of type 'Wrapper<Wrapped>' to return type 'Wrapper<Member>'
			// error: cannot convert value of type 'Member' to expected argument type 'Wrapped'
			// fix-it: Insert ' as! Wrapped'
		}
		set { wrappedValue[keyPath: keyPath] = newValue.wrappedValue }
	}
}

I think what I'm trying to do should be possible, because SwiftUI does it. Starting from 38:04 in the talk "Modern Swift API Design" from WWDC 2019, it shows that SwiftUI's Binding's dynamic member lookup returns a new Binding:

@propertyWrapper @dynamicMemberLookup
public struct Binding<Value> {
	...
	public subscript<Property>(
		dynamicMember keyPath: WritableKeyPath<Value, Property>
	) -> Binding<Property> {
        	...
	}
}

What am I doing wrong?

Weird, if you explicitly add the type parameter it compiles, right?

@propertyWrapper @dynamicMemberLookup
struct Wrapper<Wrapped> {
  var wrappedValue: Wrapped
  
  subscript<Member>(
    dynamicMember keyPath: WritableKeyPath<Wrapped, Member>
  ) -> Wrapper<Member> {
    // here: use Wrapper<Member> explicitly
    get { Wrapper<Member>(wrappedValue: wrappedValue[keyPath: keyPath]) }
    set { wrappedValue[keyPath: keyPath] = newValue.wrappedValue }
  }
}

Just tried it. Yep.

filed SR-14029

1 Like

For generic declarations, using the self type (Wrapper) or its member type (Wrapper.XXX) infers the generic parameter (Wrapped) to match. This has pretty high priority and usually cause the typechecking to fail when the generic parameter is meant to be something else, Wrapped <~ Member in this case.

Can't remember if it's intentional, though.

1 Like

It seems that this is the case for all generic types, not just generic property wrappers:

@dynamicMemberLookup
struct T<Foo> {
	let foo: Foo
	subscript<Bar>(
		dynamicMember keyPath: WritableKeyPath<Foo, Bar>
	) -> T<Bar> {
		T(foo: foo[keyPath: keyPath])
		// error: Cannot convert return expression of type 'T<Foo>' to return type 'T<Bar>'
		// error: Cannot convert value of type 'Bar' to expected argument type 'Foo'
		// fix-it: Insert ' as! Foo'
	}
}

This is interesting. Is there a benefit from the high priority?

I'm sure I asked the same question years back, if only I could find it... IIRC, it was due to how this inferred default got applied before anything else in the typechecker.

My personal take is that you need to be watchful anyway if you're in a context with Wrapped, but Wrapped means something else (Member).

To illustrate my point:

struct Foo<Value> {
  var value: Value
  typealias Member = Binding<Value>

  // Error
  func fail1() -> Foo<Int> { Foo(value: 3) }
  func fail2() -> Foo<Int>.Member { Member.constant(3) }

  // Ok
  func ok1() -> Foo<Int> { Foo<Int>(value: 3) }
  func ok2() -> Foo<Int>.Member {Foo<Int>.Member.constant(3)}
}

That said, you can get away pretty easily if you avoid using the name Wrapper:

get { .init(wrappedValue: wrappedValue[keyPath: keyPath]) }

Thanks! The example makes it much easier to understand what is going on. So basically, everywhere in Foo<Value> itself, Foo is always interpreted as Self. This seems really like a bug to me.

This issue was previously discussed in Using the bare name of a generic type within itself.

Money quote from @Douglas_Gregor:

It seemed like a nice convenience when I cargo-culture it years ago (we didn’t have the general Self at the time). I’d love to phase this out of the language.
https://forums.swift.org/t/using-the-bare-name-of-a-generic-type-within-itself/24778/26

3 Likes