SE-0299 (second review): Extending Static Member Lookup in Generic Contexts

Hello Swift community,

The second review of SE-0299, Extending Static Member Lookup in Generic Contexts begins now and runs through February 15, 2021. The previous review thread is here. By comparison with the previous revision of the proposal, this version adds an additional requirement that static members in protocol extensions have Self == ConcreteType constraints matching the types of the members in order for contextual member lookup to find them.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

Joe Groff
Review Manager

15 Likes

I'm somewhat happy with the new restriction requiring a Self constraint, but I'm worried that making that unconditional is going to be unnecessarily limiting on the case where you're just building a value of protocol type. Or does that not work because we don't know what to bind Self to?

I would suggest the rule that the variable type / function return type should be required to be a subtype of the Self type.

6 Likes

If I'm understanding this revision correctly, this proposal derives the base type for the implicit member expression entirely from the type of Self in the same-type constraint, and not at all from the particular member—is that correct?

I.e., would this now be allowed?

protocol P {}
struct S: P {}
struct R: P {}

extension P where Self == S {
  static var r: R { R() }
}

extension R {
  var s: S { S() }
}

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

test(.r.s) // Ok

Also, does this proposal use the semantics mentioned in the previous review thread?

?

I.e., what gets printed in this example?

protocol P {}
struct S: P {}

extension P where Self == S {
  static var s: S {
    print("s via P")
    return S()
  }
}

extension S {
  static var s: S {
    print("s via S")
    return S()
  }
}

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

test(.s) // ?

IMO this revision results in more explainable behavior, and is more inline with existing inference rules in similar positions (such as the examples @Joe_Groff posted here).

If the example above prints "s via P" then I'm fully on board with this revision.

If the example above would print "s via S" then I remain concerned about the ability of a declaration guide type-checking that does not appear in the final expression. The author of P would not, for instance, be able to remove P.s by deprecating the declaration for a time and then deleting it, because they could not be sure that all clients depending on the declaration would actually see the deprecation warning before removal.

I think it makes sense to keep allowing declarations without restrictions like that because it's not clear for what purpose it might be used in the expression context (i.e. it would be still possible to reference it with concrete base type). In the call site of the leading-dot syntax it would be enforced that base is equal to result type of the chain, so there is going to be a decent diagnostic for the cases where result type of the member didn't line up with Self .

1 Like

Yes, that would be allowed since base and result are the same, what happens in the middle no longer matters as soon as base and result line up.

Yes, it would print s via P since we'd use declaration found on extension P where ..., there is no additional lookup as we have discussed during previous review.

2 Likes

Awesome. Thanks for clarifying that for me. In that case, I'm super happy with where this proposal landed and give it a hearty +1! This would be a great ergonomic improvement for implicit member syntax. :slight_smile:

1 Like

I'm very happy with where this ended up. I would love to see metatype extensions explored someday but this is a nice solution to support generic dot-shorthand until that happens.

9 Likes

I’m reasonably satisfied that the revised proposal avoids the most deleterious consequences of namespace pollution as pointed out by the core team. It is still somewhat unfortunate that the concrete type does have a not-very-useful member; I wonder if there might not be some ad-hoc rule that could be devised to prevent its appearance in autocomplete lists.

The problem is worth solving and I think it would certainly improve user ergonomics to solve it. I’ve followed this proposal through its various stages and put in some time to consider the revised proposal.

One specific suggestion I have is that this particular feature should be explicitly documented in TSPL under the section on dot expressions, and elsewhere where appropriate. The way in which the feature is expressed in code is not exactly easily discoverable, so it should be called out to users for their understanding.

9 Likes

Yes, I concur! We'll work on updating the documentation.

6 Likes

Should the following work?

extension ToggleStyle where Self == CustomToggleStyle {
  init(custom: Int) {
    /// An initializer defined on CustomToggleStyle
    self.init(...)
  }
}
let toggleStyle = ToggleStyle(custom: 42)
1 Like

This proposal is scoped to leading dot syntax only, behavior of other code would stay the same.

1 Like

At first glance, this proposal seems like the wrong way to achieve its goal, and an abuse of the type system. Now that we have property wrappers in function arguments (or is that still pending?) I think the right way to achieve this would be for SwiftUI functions to take an enum that they automatically transform into an instance of the correct type. It would look something like this:

enum ToggleStyleEnum {
    case `default`
    case `switch`
    case checkbox
    case other(CustomToggleStyle)
}

@propertyWrapper
struct ToggleStyleWrapper {
    init(_ ts: ToggleStyleEnum) {
        switch ts {
            ...
        }
    }
}
    
function toggleStyle(@ToggleStyleWrapper toggleStyle: ToggleStyleEnum) {
    ...
}

It's interesting, because I view the solution from this proposal as a natural extension of existing lookup rules, and the property-wrapper-based solution as an abuse of the type system! :smile:

I don't think we should encourage the pattern of using argument wrappers as implicit arbitrary-type-conversion operators at function argument boundaries.

8 Likes

I still really want extensions on the meta type, but as long as this doesn't block that from being added in the future, this seems like a fine addition.

5 Likes

I'll still complain that these declarations are overly complicated and their intended usage is hard to decipher:

extension ToggleStyle where Self == DefaultToggleStyle {
  public static var `default`: Self { .init() }
}
extension ToggleStyle where Self == SwitchToggleStyle {
  public static var `switch`: Self { .init() }
}
extension ToggleStyle where Self == CheckboxToggleStyle {
  public static var checkbox: Self { .init() }
}

The language would express the intent better using a new keyword:

extension ToggleStyle {
  public factory var `default`: DefaultToggleStyle { .init() }
  public factory var `switch`: SwitchToggleStyle { .init() }
  public factory var checkbox: CheckboxToggleStyle { .init() }
}

I think I'd be fine if this was just sugar over the current proposal. It'd be unfortunate however if it was introduced first without the sugar since that's how people will start teaching the feature to each other.

3 Likes

This is what I was going for with my enum + function builder example, but 100x better.

I like that notation, but don't think it should be sugar for the current proposal.

It would work well as a more intuitive notation for placing functions on the meta type. I would also love to see factory inits that allow you to build and return any subtype of the protocol!

What would happen here? Would we disallow re-declaring foo or complain that .foo is ambiguous?

extension ToggleStyles where Self == SwitchToggleStyle {
  static func foo: Self { .init() }
}
extension ToggleStyles where Self == CheckboxToggleStyle {
  static func foo: Self { .init() }
}
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
  .toggleStyle(.foo)

That ought to raise an ambiguity error, since there's no single type to infer from the member. That's what would happen if you did the same thing with constrained extensions on a generic type.

8 Likes

@Joe_Groff This was merged 6 days ago so it's approved right?
https://github.com/apple/swift/pull/34523