[Proposal] Static member lookup on protocol metatypes

Can you elaborate on this? There are certainly many members (including static members) that might be declared in a protocol extension that do not return a type conforming to the protocol. This is true of code that already exists today and which has nothing to do with the convenience introduced by this proposal.

2 Likes

Because, hopefully, there's a relatively clear correspondence between what's written in the source and the type-checked AST that that source produces. Currently, T.member always refers to a member of the metatype of T. Introducing a break from that convention just for protocols, especially if we plan to (eventually) give P.member the conventional meaning, is more than a little troubling to me.

Even without considering future plans for bona-fide members on protocol metatypes, though, I think the the proposed behavior for P.member is problematic. For one, its syntax certainly suggests that protocol metatypes can have members, so IMO it's surprising to find out that that's not what's going on. Secondarily, the rule for figuring out what type actually gets substituted at the base of the access is pretty subtle, and a bit to implicit for my tastes.

I don't see why this is the case. The inability to reference P.member when member does not conform to P seems like an arbitrary restriction that further distances this member access syntax from the existing precedents in the language.

I'm only talking about the new rule proposed in this pitch - I think it makes most sense to have that requirement be local to the member declared on a protocol If I want to reference it as a static member on that same protocol, chaining would be done on instance members of the type returned from first member.

I think you might be confusing a declaration with reference - it's possible to declare a computed static member on a protocol metatype today, that's exactly what we are using. But it's only possible to reference it through a metatype of a conforming type. I think replacing the base type (which isn't even written in the source when it comes to a leading dot syntax) is the least invasive way to form a correct reference to a static member of a protocol metatype with current language semantics, and new rule has to be centered on the declaration itself because chaining is not always involved and it would be not very clear to make reference to a particular member be dependent on result types of other members referenced in the chain. Having member itself require conformance to the declaring protocol to be referenceable through a metatype of that protocol is the least invasive rule we could come up with.

That may be how a static member in a protocol extension is implemented, but as far as the programmer concerned that seems to me like a distinction without a difference. If the static member on P itself is completely inaccessible in any context, I don't know what it means to claim that it's really been declared.

So, as an example where this distinction matters, consider:

protocol SelfDescribable {}

extension SelfDescribable {
    static var typeDescription: TypeDescription { TypeDescription(description: String(describing: Self.self)) }
}

struct TypeDescription: SelfDescribable {
    var description: String
}

struct S: SelfDescribable {}

print(S.typeDescription.description) // prints 'S'

If I'm understanding this proposal properly, then under this proposal we would have the following behavior:

print(SelfDescribable.typeDescription.description) // prints 'TypeDescription'; huh?

I think it's actively misleading to have something written as P.member when Self inside of member's accessors does not reference P.

Cases where there's true ambiguity would be rejected by the compiler, so in valid chains there should only be one correct 'path' from the base to the tail of the chain. In any case, I expect the vast majority of cases will involve only a single member (in which case the distinction between first and last member is moot) or will have the same type along the whole length of the chain, so this point probably doesn't make a huge difference in practice.

It is accessible through a metatype of a conforming type.

Help me understand why this is misleading. The rule states that SelfDescriptive.typeDescription acts as if it was TypeDescription.typeDescription, so based on the rule we are proposing, such behavior would be correct. Self is indeed TypeDescription in that case, S.typeDescription has a Self of S so that behavior is also consistent. I wonder why declaring such members is useful though.

We could adjust the proposal to reject expressions which specify protocol metatype explicitly and allow only leading dot syntax, but we thought that it might be useful to have that be consistent.

I just want to mention that P.Type is already special and it refers to a metatype of a type that conforms to P and P.Protocol refers to a protocol metatype itself. In reference we use <protocol>.Type and not <protocol>.Protocol so expressions like P.foo mean P.Type[.foo].

I understand that, but I don't view that as being accessible "on P itself". There is no member accessible to the user which, when invoked, has self == P.self.

I'm not claiming that the proposed rule is broken or inconsistent, just that it results in the ability to write code which does not do what it most obviously appears to do. Today, writing P.member unambiguously indicates that member is invoked on P.self. This proposal would break that assumption in a very non-apparent way.

Yeah, I think this suggestion from @anandabits would be a great improvement. In all dealings with implicit member syntax, I find it really helpful to frame things in terms of the question: "what is the non-implicit spelling of this expression?". So with the following setup:

protocol P {}
struct S: P {}
extension P {
  static var member: S { S() }
}

func test<T: P>(_: T) {}

if I've understood everything correctly thus far, the main goal of this proposal is to be able to properly type check test(.member). The non-implicit version of that expression is spelled as test(S.member), and IMO that's the expression that we should expect users should write instead of test(.member) (in cases where, say, there's not enough type context to use implicit member syntax).

In particular, I don't see any need to introduce a new sort of member access on protocol metatypes into the language surface. That P.member is how the constraint system models the above member access during solving just becomes an implementation detail.

3 Likes

Ok, sounds good! If that makes more sense we'd adjust the proposal to allow only leading dot syntax and reject references with explicit protocol metatype base with the same diagnostic we do today. After all, leading dot syntax is the most interesting use-case here.

3 Likes

Great! That sounds like a very reasonable model to me. I still have my gripes about requiring the first member in a chain to conform to the protocol, but I've said my piece so I'll save any additional commentary for the review thread. :wink:

Only additional thing I'll say is that if we stick with the current rule, then this section:

ought to be replaced with language that clearly and unambiguously describes the circumstances under which the member in question is considered satisfactory.

I guess we could use the language I have mentioned and add a remark about what result type means just like it was done for multiple trailing closures.

Right, we wouldn't need a worry about partial applications so we'd have to clarify the language there, thanks for pointing it out!

2 Likes

That all sounds great. Thank you for sticking with me through that discussion! Overall, I think this a great ergonomic improvement.

2 Likes

Thanks for all the input!

Agree. With the ability to constrain the protocol extension to the return type and no ability to explicitly access members via the metatype, I think this is a good way to enable some sugar before true metatype extensions are available. I look forward to having it available!

7 Likes

Thanks for the feedback! We're working on upgrading the pitch to a full proposal, with changes based on the discussion here, which we hope to publish soon.

A few final bits of follow-up:

To clarify: we wouldn't ever remove these symbols, we would deprecate-and-replace them. For cases where the client is using leading dot syntax they hopefullty wouldn't notice any difference; in the few cases where someone might be using a more explicit form no longer preferred, they would get a deprecation warning (likely with a fix-it).

Agreed, our goal here is a narrow carve-out, and this discussion has helped us narrow it even further, thanks!

5 Likes

This proposal has been scheduled for review as SE-0299, with review starting next Monday, January 18.

15 Likes