Every step after step 1, since only information available to the compiler is that (contextually) there should be a type that conforms to P that is a supertype of a result of a chain. Compiler doesn't know anything about .someS or .r until it actually resolves the first member reference with some base type, so we can't actually just assume that it's possible to resolve members backwards.
This is actually consistent with what happens in non-generic leading dot syntax - I emphasize - that contextual type from the selected overload is propagated to the base and lookup is done forward and not backward from the result.
Why isn't the knowledge that T conforms to P sufficient to know that someS will be of type S? Is there any situation where the type chosen for T will somehow result in the witness for someSnot having type S? Sure, we cannot resolve the exact witness for someS until we have the fully-resolved type, but it seems to me like the exact witness is irrelevant for determining the type of the whole chain.
The proposal as-written is already introducing the back-propagation from member type to base type, so I don't really see why the back-propagation shouldn't apply to multi-member chains as well as single-member chains.
ETA: if it were possible to reference a generic function argument from within the context of the caller, I would think of takesP(.someS.r) as basically analogous to writing takesP(T.someS.r). This is a perfectly well-formed expression within the body of takesP, and the type resolves to R.
Because conforming type case override static method on a protocol, if there is a default implementation we'd have to consider that and the override so it's impossible to determine whether a particular member would return the type without actually checking.
I agree with Benjamin. The problem is that static requirements declared in a protocol are not static members on the protocol type, and we/I keep confusing them. It seems more clear to differentiate them and spell the later as:
extension ToggleStyle.Type {
public static var `default`: DefaultToggleStyle { get }
public static var `switch`: SwitchToggleStyle { get }
public static var checkbox: CheckboxToggleStyle { get }
}
This would allow dot member lookup to work to solve the SwiftUI problem, solves Jordan's top level concern about type pollution, would provide a solution to the let x = Type.member vs let x: Type = .member consistency issue (by making them both look in the metatype instead of static requirements) and would also nudge the whole existential / PAT / self conformance discussion in an interesting way.
Are you talking about a situation where we additionally have something like:
extension S {
static var someS: R { R() }
}
?
I was trying to propose semantics which sidestep this issue altogether: at the point where type inference has to use the P conformance to resolve someS (since we don't have the concrete type yet), someS would be fixed to refer to the witness for P.someS, so we couldn't later consider a different overload from whatever the concrete type ends up being.
This would be similar to how from within takesP, T.someS can only refer to the witness for P.someS, and won't use a shadowed someS member on T even if at runtime the generic argument for T has such overloads.
I pulled this discussion into private with @xedin to reduce noise in the review thread, and I think I now have a much better grasp of the semantics here as well as my discomfort with them! Just going to drop a brief summary here in case anyone else faced the same confusion as myself.
@xedin provided a useful example which I think is illustrative:
protocol P {
static var foo: Foo {
print("foo via P")
return Foo()
}
}
struct Foo : P {
static var foo: Bar {
print("foo via Foo")
return Bar()
}
}
struct Bar : P {
var bar: Bar { Bar() }
var baz: Foo { Foo() }
}
func test<T: P>(_: T) {}
test(.foo.bar.baz)
The main gap in my understanding of the semantics here was not realizing that that the static member lookup on the protocol metatype is only used to determine the base type of the expressionâafter that is determined, there's a second-pass lookup of the static member on that base type, which may not end up referencing the same declaration.
So in the example above, the expression test(.foo.bar.baz), prints "foo via Foo", since .foo references Foo::foo and has type Bar. The P::foo declaration is only used for finding the Foo type to insert at the implicit base, and does not appear in the type-checked expression.
IMO, having the type inference be influenced by the type of a declaration that doesn't end up in the final expression is pretty unintuitive. So I'm still a -1 on these semantics.
ETA: and of course, @xedin, correct me if it seems like I've misstated anything here.
Thanks for writing up a summary of our discussion! I'm okay if the semantics are changed to use declaration reference found on protocol metatype (this is actually what implementation as-of-today does), but I'm not okay with picking base via backtracking from the result of a chain, besides being confusing I don't see a good way to implement that in the solver.
I think that for sure results in more explainable outcomes, personally! Though I'd love to hear others chime in as wellâI'll bow out of this thread for a while now that I feel like I actually know what's going on.
Totally reasonable! I'll have to decide if I want my semantics badly enough to try to implement them myself.
I'm now thinking that maybe the proposal will be good, with one change, and a different convention.
The change:
We've gone over why that's problematic. Can it go elsewhere? I think that having a Swift-private type* (maybe even the same one for every use case) would be fine. A programmer will never need to use it.
Whether this works is dependent on how relaxed the rules are going to be, from the current "what comes after the leading dot must match what would come before the dot, exactly".
The convention:
Above, there are instances of using where Self == clauses to minimize incorrect member inclusion. What if we instead put the static members into an extension for types that don't actually exist (in a usable location)? The result would be complete elimination of incorrect member inclusion.
It seems to me that the code below still follows the laid-out rule�
/// A protocol that is `enum`-like,
/// in that the types that implement it are groupable options.
public protocol ProtocolWithCases { }
public extension TabViewStyle where Self: ProtocolWithCases {
static var `default`: DefaultTabViewStyle { .init() }
static func page(indexDisplayMode: PageTabViewStyle.IndexDisplayMode) -> PageTabViewStyle {
.init(indexDisplayMode: indexDisplayMode)
}
#if os(watchOS)
static var carousel: CarouselTabViewStyle { .init() }
#endif
}
I think it's better to have something like a ProtocolWithCases that we all use for the same purpose, but any restriction to imaginary (except to the compiler) types will do.
[*] A possible implementation, where synthesized extensions get grouped similarly to source:
private enum _ProtocolWithCases<Case>: ProtocolWithCases { }
// Extensions would get synthesized by the compiler every time a
// new static member is added to `ProtocolWithCases`.
extension _ProtocolWithCases: TabViewStyle where Case: TabViewStyle {
static func _makeView<SelectionValue: Hashable>(
value _: _GraphValue<_TabViewValue<Self, SelectionValue>>,
inputs _: _ViewInputs
) -> _ViewOutputs {
fatalError()
}
static func _makeViewList<SelectionValue: Hashable>(
value _: _GraphValue<_TabViewValue<Self, SelectionValue>>,
inputs _: _ViewListInputs
) -> _ViewListOutputs {
fatalError()
}
}
You would have to elaborate on how call site for this idea is going to look like since the idea is to hide _ProtocolWithCases (if I understand correctly), so I'm not sure how it's going to be used?...
How about we just exclude results like that code completion since these members are supposed to be accessed via a particular syntax. In combination with Self == <Type> declaration there is going to be no visible effect for the API users unless they actually write out DefaultToggleStyle.default explicitly.
Sounds secret enough to be practically usable. Somebody will probably run into a naming collision, but I bet that would have been a confusing name anyway.
All this said, I do think that being able to discover these types outside of the context of generic methods would be helpful. Their initializers represent the same idea as enumeration cases (without exhaustibility).
Chris's idea here is good, but it, along with the original StaticMember, represents a larger scope than what we're specifically dealing with, which is an alternate way of accessing initializers.
You can almost represent what's needed, right now, with typealiases:
public enum TabViewStyleCase {
public typealias `default` = DefaultTabViewStyle
#if !os(macOS)
public typealias page = PageTabViewStyle
#endif
}
public extension TabViewStyle {
typealias Case = TabViewStyleCase
}
If we have to choose one, then I think I would prefer having it on the meta type.
The factory let and factory func syntax was compelling. It doesn't have to be the word factory, but I like the idea of decorating the function (as opposed to the extension)
(To be clear, I would also happily accept the extension MyType.Type if that is what will get us this feature. As I said above, I run into the need for this near-constantly)
Could you clarify why this is a language feature rather than, say, a SourceKit feature? Say something along the lines of:
You type a leading dot at an argument position where the type is generic.
If there are no protocol requirements, SourceKit does nothing.
If there are protocol requirements, SourceKit looks up which types are declared to conform to protocol requirements and pass on the list to the editor for completion.
When a completion is chosen, the editor deletes the previous dot and inserts the selected type.
I'm reading through the comments here and re-reading the proposal, and it seems like all of the discussion is focused on describing how we could make this work as a language feature. As I understand it, the primary benefit is in helping someone write code [speaking as someone who just started learning SwiftUI last week and hit this issue of "wait what do I put here???"], and the benefit for readability is not that big (IMO).
Implementing this as a SourceKit feature has three benefits:
We don't need to change the language. If people still feel that this really impairs readability, then we can revisit the issue at a later stage.
It avoids the weird edge cases that other people have pointed out, such as this one.
func squared<T: FixedWidthInteger>(_ x: T) -> T? { ... }
let squaredBitWidth = squared(.bitWidth) // picks T == Int
Library maintainers don't have the additional burden of such similar static members for protocols they are vending.
If these members are defined on the metatype (ToggleStyle.Type) rather than the existential type (any ToggleStyle), then they shouldn't also still be static members, should they?
But besides that point, I do think the approach you mention is a superior one to the proposal's.
I would echo the general tenor of the feedback here that, while the problem is worth tackling, I don't think the proposed solution meets the challenge as it introduces some difficult-to-understand rules to an already difficult-to-understand feature (dot expressions). I honestly cannot imagine any Swift user besides the proposal's authors being able to explain correctly how dot expressions work in full if the proposed design is adopted. The rest of us will have to apply "approximations" of the true rules that sometimes don't align with reality.
@typesanitizer I think the only way to accomplish what you are proposing would be implement a type-checker feature and expose it through SourceKit only requests. SourceKit itself can't possibly implement all of the checks required here - it would have have to transitively pass protocol requirements through chains of members to the base, handle overloaded calls, disambiguate etc. and methods would still have to be declared in a way we propose which is the main point of contention here.