Is this behaviour of Protocol Static Member Lookup intentional

The following codes can build and run successfully. However, the same-type constrain on Self in the extension to ExampleProtocol does not match the return value of the static function and can still be accessed by leading dot syntax. Is that an intentional design in Swift?

protocol ExampleProtocol {
    associatedtype T
    var value: T { get }
}

struct Example<T>: ExampleProtocol {
    let value: T
}

extension ExampleProtocol where Self == Example<Int> {    // constraint Self to Example<Int>
    static func example<T>(_ value: T) -> Example<T> {
        return .init(value: value)
    }
}

func test(_ value: any ExampleProtocol) {
    print(value.value)
}

test(.example("a"))       // used as Example<String>
1 Like

There is actually two different generic types in this example: associatedtype T and example’s generic T. So you actually call correct extension method, but with its own generic parameter. You need to write func example(_ value: T) so it uses protocol’s type.

I'm aware of that, but I thought when using the leading dot syntax to access static members on a protocol, the return type of the member should match exactly with the constrained type on Self. So in my current understanding, .example("a") here should not be allowed.

Actually I can change the definition of the example function as follow:

extension ExampleProtocol where Self == Example<Int> {
    static func example(_ value: String) -> Example<String> {
        return .init(a: value)
    }
}

This is still valid code. But if this is valid, why we need the same-type Self constraint at all?

1 Like

Nothing prevents from using dot-syntax on some ExampleProtocol<Int> type with String — that is perfectly fine, since generic type is different. You can think of that as calling Example<Int>.example("a") — it is perfectly sound with constraint.

The constraint disallows writing Example<String>.example(1) for example, since extension is available only for Example<Int>

The version you probably looking for is

extension ExampleProtocol where Self == Example<Int> {
    static func example(_ value: T) -> Example<T> {
        return .init(a: value)
    }
}

Thanks! I get it now. I'm not really looking for a specific version for this, just found this behaviour accidentally and was trying to understand it.

1 Like

I think this is a bug with no use case. (I can't tell if the explanation at the end of the design covers this case or not.)

You can rewrite your function like this, and it will compile:

func test(_ value: some ExampleProtocol) {
  print(value.value)
}

But there is no actual concrete type for which it will compile—making "some" contractually invalid.


Extra horrible:

Because the nonsensical call to test compiles, this call to test is considered ambiguous.

extension ExampleProtocol {
  static func example<T>(_ value: T) -> Self where Self == Example<T> {
    .init(value: value)
  }

  static func example<T>(_ value: T) -> Example<T> where Self == Example<Int> {
    .init(value: value)
  }
}
func test(_ value: some ExampleProtocol) { }
test(.example("a"))

(Overloading as intended fixes the problem.)

extension ExampleProtocol {
  static func example<T>(_ value: T) -> Self where Self == Example<T> {
    .init(value: value)
  }

  static func example(_ value: Int) -> Self where Self == Example<Int> {
    fatalError("Specialization not implemented yet")
  }
}
test(.example("a"))
test(.example(1))

Can you elaborate on what you mean by this?

func test(_ value: some ExampleProtocol)

is sugar for:

func test<EP: ExampleProtocol>(_ value: EP)

which means the actual concrete type is determined at the call site, since .example() constrains the generic to the concrete type Example:

test(.example(...))

am I missing something?

1 Like

EP: ExampleProtocol or some ExampleProtocol each mean "one type that conforms to ExampleProtocol".
Example is generic. Example<Int> and Example<String> is not one type. It is two.

Example<Int> has no business being used for resolution of .example("a").


For clarity, these are the two possible relevant function specializations.

test(.example("a")) will not compile for either of them, given the original code. This is appropriate behavior.

func test(_: Example<Int>) { }
func test(_: Example<String>) { }

So in Swift's type system... afaik, Example<Int> and Example<String> are individual types. For you, what are the costs of treating them as one type, and the benefits of changing those semantics?

(I feel like we're hijacking the thread at this point but I'm genuinely curious)

I'm rereading swift-evolution/proposals/0299-extend-generic-static-member-lookup.md at main · swiftlang/swift-evolution · GitHub, and technically it just says the Self type for contextual lookup should be a concrete type, not necessarily the same concrete type that ends up getting used. I do think it would make more sense if it was required to be the same though! Let's make it really weird:

protocol Example {}

extension String: Example {}
extension Int: Example {}

extension Example where Self == String {
    static func number() -> Int { 0 }
}

func accept(_ input: any Example) {}

func test() {
    accept(.number())
}

No generics at all, and yet this compiles.

4 Likes

Guarantee someone's actually using this behavior and making any change would be actually source-breaking.

3 Likes

The only possible reason to allow this is if types could be nested within protocols. Then you could have namespace chains. (All of the namespaces could be nested inside of one protocol adopter.) There is no use case for non-nested static members with differing types to use leading dot syntax.

What we have right now is nonsense, inconsistent with the non-protocol version of the same idea:

struct Example<T> {
  enum Namespace { }
}

extension Example<String> {
  static var matchingExample: Example { .init() }
  static var nonMatchingExample: Example<Int> { .init() }
}

extension Example.Namespace {
  static var stringExample: Example<String> { .init() }
  static var intExample: Example<Int> { .init() }
}

func accept(_: Example<some Any>) { }
// All compile.
accept(.matchingExample)
accept(.Namespace.stringExample)
accept(.Namespace.intExample)

// ❌ Member chain produces result of type 'Example<Int>' but contextual base was inferred as 'Example<String>'
accept(.nonMatchingExample)

Edit:

It's not as bad as I thought—namespacing is possible, with a type alias. There's just currently no point in defining it anywhere but in the constrained protocol extension, because autocomplete doesn't pick it up in any case whatsoever—which is a bug not present in the above, generic form.

protocol Example { }

extension String: Example { }
extension Int: Example { }

enum _Namespace: Example { }

extension Example where Self == String {
  static var matchingExample: Self { .init() }
  static var nonMatchingExample: Int { .init() }
  typealias Namespace = _Namespace
}

extension Example.Namespace {
  static var stringExample: String { .init() }
  static var intExample: Int { .init() }
}

func accept(_: some Example) { }
// All compile.
accept(.matchingExample)
accept(.Namespace.stringExample)
accept(.Namespace.intExample)
accept(.nonMatchingExample)

I thought of a loophole, with protocols which conform to themselves.

This will compile…

accept(.object)
_ = .object as any Protocol

…even though there's no actual type which can offer this object property:

@objc protocol Protocol { }
final class Class: Protocol { }

extension Protocol where Self == any Protocol {
  static var object: Class { .init() }
}

func accept(_: some Protocol) { }

Similarly, there's no actual type which offers e, here:

struct E: Error { }

extension Error where Self == any Error {
  static var e: some Error { E() }
}

func throwE() throws { throw .e }

I don't agree that this should compile, but now that I know it does, I wouldn't want to lose the ability to do this:

import Metal

public extension MTLDevice where Self == any MTLDevice {
  static var `default`: Self { MTLCreateSystemDefaultDevice()! }
}

public extension MTLLibrary where Self == any MTLLibrary {
  static func `default`(bundle: Bundle) throws -> Self {
    try (.default as any MTLDevice).makeDefaultLibrary(bundle: bundle)
  }
}
func f(_: some MTLLibrary) { }
try f(.default(bundle: .main))

The general form of my previous workaround looked like this:

import Foundation

@objc protocol Protocol { }
final class Namespace: Protocol { }

extension Protocol where Self == Namespace {
  static var existential: any Protocol {
    fatalError("Return something that is a Protocol but not a Namespace")
  }
}

func f(_: some Protocol) { }

func callF() {
  f(.existential)
}

The Namespace-style classes were never necessary. I hadn't thought before about the only reason for them working was that the protocols I was working with were all from Objective-C, because the only APIs I regularly deal with that vend so many existentials are written in Objective-C.

I.e. remove @objc from the code above, and you get
Type 'any Protocol' cannot conform to 'Protocol'
unless you also use any instead of some.

Without Objective-C, there's still no way to vend a leading-dot static existential or opaque instance without a "namespace type" (_Fake, here):

protocol Protocol { }
enum _Fake: Protocol { }
struct Real: Protocol { }

extension Protocol where Self == _Fake {
  static var existential: any Protocol { Real() }
  static var opaque: some Protocol { Real() }
}

extension Protocol {
  typealias Namespace = _Fake
}

var existential: any Protocol { .existential }
var opaque: some Protocol { .opaque }
var namespacedExistential: any Protocol { .Namespace.existential }
var namespacedOpaque: some Protocol { .Namespace.opaque }