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>
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:
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>
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.
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)
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 }