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.
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.
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.
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.
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!
That all sounds great. Thank you for sticking with me through that discussion! Overall, I think this a great ergonomic improvement.
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!
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).
I would consider them mildly harmful in some contexts. For that reason, I think the return type restriction is actually a good thing. I don't think we want people using the protocol as a namespace for arbitrary static API related to the protocol itself (rather than conforming types) until that API can be isolated to the metatype. I think a narrow carve out for advanced library developers to support dot shorthand is as far down this path as we should go.
Agreed, our goal here is a narrow carve-out, and this discussion has helped us narrow it even further, thanks!
This proposal has been scheduled for review as SE-0299, with review starting next Monday, January 18.