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

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

Yes, the core team has accepted this proposal, with the revision suggested by John at the start of the thread.

4 Likes

Thought I'd drop a quick note to highlight a possible failure case for this proposal's implementation, described by [SR-15853] Re: SE-0299: Static member lookup fails in type that has two tuples. · Issue #58127 · apple/swift · GitHub.

IIUC the code you've posted in that bug doesn't introduce the proper generic context for the static members to be resolved under SE-0299, since Model.a.y and Model.b.y are of existential type Tint rather than generic type constrained to Tint. Changing the definition of Model to the following allows this to type check:

struct Model<T: Tint> {
    let a:   (x: String, y: T)?
    let b:   (x: String, y: T)?
}

ETA: though this still appears to be a bug with implicit member syntax in general since this works with the non-tuple and single-tuple versions. I just don't think it's related to SE-0299 specifically.

Thanks, that makes some sense. Though with that said, if not under SE-0299's implementation, perhaps you can clarify for me how the swift compiler resolves the member type of the existential, then? Note that the requirements on how the members should be declared on the protocol extension are exactly in line with SE-0299.

1 Like

Actually, after looking into it a bit further, I take it back—I think this is related to SE-0299. This sort of inference through an existential was not permitted prior. I was confusing it with the behavior that allowed the result of a chain to be a subtype of the overall result type for subclasses.

cc @xedin is the lookup behavior in an existential context intended here?

SR-15853 looks like a bug to me which was been fixed recently, I have tried with 02/03 snapshot of main and the example type-checks correctly with that toolchain.

1 Like

Ah, I didn't realize that SE-0299 was also intended to work in existential contexts—I thought it was restricted only to generic contexts. Perhaps the proposal title/body should be adjusted to make it clear that this use case is supported?

I understand what you mean although it took me a bit :) Strictly speaking it's indeed not to the letter of the proposal since it would infer a base type not based the generic conformance requirement of the result, but directly from the protocol of the existential type, so maybe it indeed makes sense to document that somehow...

1 Like