Static member lookup on protocol with associated type

I'm running into an unusual issue trying to implement static member lookup for a protocol with an associated type:

protocol Foo<Bar> {
  associatedtype Bar
}
struct ConcreteFoo<Bar>: Foo {
  
}
/// Same-type constraint 'Self' == 'ConcreteFoo<Self.Bar>' is recursive
extension Foo where Self == ConcreteFoo<Bar> {
  static var concreteFoo: Self { ConcreteFoo<Bar>() }
}

Seems like this should be possible... is there any way to work around this?

1 Like

Can you use a function instead?

extension Foo {
  static func concreteFoo<Bar>() -> Self 
  where Self == ConcreteFoo<Bar> {
    Self()
  }
}
1 Like

It is not entirely clear to me what exactly you are trying to achieve. It looks like you are making an extension specific to the type ConcreteFoo, so why can’t you directly extend that type?

1 Like

This is, sadly, the only option. The official example isn't explicit enough about that, but the wording is accurate:

public struct CustomToggleStyle<T>: ToggleStyle {
  ...
}

extension ToggleStyle {
  public static func custom<T>(_: T) -> Self where Self == CustomToggleStyle<T> {
     ...
  }
}
2 Likes

That's can be pretty useful. Suppose you have a generic function accepting a type conforming to a protocol:

func doSomething<T: Foo>(_ value: T) { ... }

By extending Foo with factory functions with Self constrained to a concrete type you can create an instance of T without specifying type on the callsite:

extension Foo where Self == ConcreteFoo {
  static var concreteFoo: Self { Self() }
}
doSomething(.concreteFoo)

But when you have an associatedtype you have to use a function because the factory becomes generic as well:

extension Foo {
  static func concreteFoo<Bar>() -> Self 
  where Self == ConcreteFoo<Bar> {
    Self()
  }
}

func doSomething<T: Foo>(_ value: T) where T.Bar == Int { ... }

doSomething(.concreteFoo())
2 Likes

The Bar constraint would allow for

extension Foo where Self == ConcreteFoo<Int> {
  static var concreteFoo: Self { .init() }
}

doSomething(.concreteFoo)

You can even have overloads compile…

extension Foo where Self == ConcreteFoo<Int> {
  static var concreteFoo: Self { .init() }
}

extension Foo where Self == ConcreteFoo<Bool> {
  static var concreteFoo: Self { .init() }
}

func doSomething(_ value: some Foo<Int>) { }
func doSomething(_ value: some Foo<Bool>) { }

…but there's no way to use them.

doSomething(.concreteFoo) // Ambiguous use of 'concreteFoo'
1 Like

That’s not quite true. You just need something external to specify the associated type. For example, the FormatStyle protocol has static properties named number, with one for each built-in integer and floating point type. In that case, it isn’t ambiguous because any time you want to use the .number member you’ve implicitly specified the corresponding Input associated type via the type of the value you want to have formatted.

2 Likes

That's a great example of static member lookup, but doesn't represent the enum-case-like syntax which allows for seeing all available options by typing "." and skimming the code completion options.

By "them", I meant pairs of concreteFoo and doSomething overloads.

I don't know what intended usage for concreteFoo looks like, but I know I've been unhappy when having to have to use methods instead of properties, due only to the language's lack of property genericism/constrainability.

It feels less bad if disambiguation is ever actually necessary, warranting a parameter.

extension Foo {
  static func concreteFoo<Bar>(_: Bar.Type = Bar.self) -> Self
  where Self == ConcreteFoo<Bar> {
    .init()
  }
}

func doSomething(_ value: some Foo) { }
func doSomething(_ value: some Foo<[Set<Never>: [SIMD16<Float16>]]>) { }

doSomething(.concreteFoo(Int.self))
doSomething(.concreteFoo())

There’s no way to express this requirement without introducing a new generic parameter. You would be able to do this if we allowed computed properties or extensions to have their own generic parameter lists, but today you have to do this with a function instead.

I remember when a protocol extension with a fixed Self was one of my favorite “haha it’s totally useless but it has a consistent interpretation” brain teasers, so you can imagine my surprise when it showed up in a legitimate feature in the form of static member lookup!

Still waiting for someone to find a use-case for the “deadly embrace” of a protocol that can only ever have a single conformance:

class C: P {}
protocol P: C {}
1 Like

P can have infinitely many conformances as long as they are all subclasses of C. (Unless “conformance” here means something other than “implementation of P’s requirements”.)

Ah but they cannot be distinct though because C conforms to P already and any subclass of C inherits the implementation of that conformance :slight_smile:

So while protocol P: NSObject makes sense alone, it’s pointless if NSObject already conformed to P.

1 Like

But in Swift, class implementations are dispatched virtually:.

protocol P: C {
  static var classProp: String { get }
  var prop: String { get }
  func method() -> String
}

class C : P {
  class var classProp: String { "C.classProp" }
  var prop: String { "C.prop" }
  func method() -> String { "C.method()" }
}

class D : C {
  override class var classProp: String { "D.classProp" }
  override var prop: String { "D.prop" }
  override func method() -> String { "D.method()" }
}

func f<T: P>(_ obj: T) {
  print(T.classProp)
  print(obj.prop)
  print(obj.method())
}

f(C())
f(D())

prints:

C.classProp
C.prop
C.method()
D.classProp
D.prop
D.method()

FWIW, I wasn’t sure if dispatch of classProp would work correctly if it wasn’t part of P. But thankfully it does:

protocol P: C {
  // Empty
}

class C : P {
  class var classProp: String { "C.classProp" }
}

class D : C {
  override class var classProp: String { "D.classProp" }
}

func f<T: P>(_ obj: T) {
  print(T.classProp)
}

f(C())
f(D())

prints:

C.classProp
D.classProp
1 Like

That's just double dispatch, though -- the conformance has one global witness table, and the entries in the witness table call into the vtable.

The distinction is that you can't override type witnesses this way:

protocol P { associatedtype A }

class B {}
class C: B {}

extension B: P { typealias A = Int }

// can't do this:
extension C: P { typealias A = String }

You also haven’t constrained P to B here, so it’s illustrating a behavior independent of the “deadly embrace” pattern.

Yep, if a type parameter is subject to a superclass requirement, members of the superclass are visible members of the type parameter. Here P doesn't have any members at all:

class C {
  func f() {}
}

protocol P {}

func f<T: C & P>(t: T) {
  t.f()
}

This just becomes an upcast (t as C).f().

Another way to think about it is that if C: P and P: C, then a conformance requirement where T: P is equivalent to a superclass requirement where T: C in every way. So the existence of P does not give you anything new.

1 Like

I’ve done this for non-static properties before with dynamic member lookup + a generic subscript. Wondering if static key paths might make this a feasible approach (though I don’t think dynamic member lookup supports static key paths) but may be too hacky of a solution :)