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

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

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