Compiler Error on Static Protocol Funcs with Self Requirements

Hi, I'm seeing a strange compilation inconsistency on the nightly toolchain when using protocol static funcs with self requirements. It's probably explained best by this code example:

func main() {
    let c: any P = C()
    
    // COMPILES
    type(of: c).arrayOfSelf()
    
    // DOESN'T COMPILE:
    // Member 'proxyOfSelf' cannot be used on value of type 'any P.Type'; consider using a generic constraint instead
    type(of: c).proxyOfSelf()
}

protocol P {
    static func arrayOfSelf() -> Array<Self>
    static func proxyOfSelf() -> Proxy<Self>
}

final class C: P {
    init() {}
    static func arrayOfSelf() -> Array<C> {
        return Array<C>()
    }
    static func proxyOfSelf() -> Proxy<C> {
        return Proxy<C>()
    }
}

// Just a basic self-defined generic. Compare with Array.
struct Proxy<T> {
    init() {}
}

Is there something special going on with Array that causes arrayOfSelf to compile while proxyOfSelf does not?

Notes:

  • This is on the nightly toolchain. The released 5.6.1 will not compile let c: any P = C() since P has self requirements.
  • I have tried with a few different generic returns. Optional compiles, EventLoopFuture does not.
2 Likes

@Slava_Pestov, I was told you might have some experience here and I'd sure appreciate your insights if you have some. Thanks!

This has to do with the fact that Array<T> is convertible to Array<U> if T is convertible to U. For example:

let a = 5 as Any
let b = [5] as [Any]

Arbitrary types, such as Proxy<T>, can't be converted because their semantics are unknown. For instance, an existential of P has a different layout than the actual P-conforming type contained in the existential, which can result in unexpected behavior at runtime. Array and other types found in the Proposed Solution of SE-0309, on the other hand, work with the compiler to achieve these implicit conversions because they are known not to rely on the memory layout of their Element (or corresponding generic parameters).

This hopefully answers the inconsistency part of your question; however, I'm not sure if your code should emit an error. The type(of:) magic function should in principle open the type contained in the existential box, meaning that any P.Type (the type of c), would become C. That holds true at runtime but I'm not sure if it should also apply at compile-time, and thus allow your example to successfully compile.

2 Likes

Oh okay, so the inconsistency is because Array (and some others) are treated as covariant, meaning:

let array: Array<any P> = Array<C>() // COMPILES
let proxy: Proxy<any P> = Proxy<C>() // DOESN'T COMPILE

I can see why this would impact my original code because the result of type(of: c).proxyOfSelf() would be a Proxy<C> cast to Proxy<any P>. Thanks for explaining.

Do you know if there are any plans to allow programmers to declare generics as covariant?

In the meantime, I think I can work around this by defining a protocol-level static func and a map function:

func main() {
    let c: any P = C()
    
    // COMPILES
    type(of: c).arrayOfSelf()
    
    // DOESN'T COMPILE:
    // Member 'proxyOfSelf' cannot be used on value of type 'any P.Type'; consider using a generic constraint instead
    type(of: c).proxyOfSelf()

    // WORKAROUND COMPILES and satisfies my needs
    type(of: c).proxyOfAnyP()
}

protocol P {
    static func arrayOfSelf() -> Array<Self>
    static func proxyOfSelf() -> Proxy<Self>
}

// New extension defines covariant casting
extension P {
    static func proxyOfAnyP() -> Proxy<any P> {
        return proxyOfSelf().map { $0 as any P }
    }
}

final class C: P {
    init() {}
    static func arrayOfSelf() -> Array<C> {
        return Array<C>()
    }
    static func proxyOfSelf() -> Proxy<C> {
        return Proxy<C>()
    }
}

// Just a basic self-defined generic. Compare with Array.
struct Proxy<T> {
    init() {}

    // New mapping function to change Proxy's type
    func map<Q>(callback: (T) -> Q) -> Proxy<Q> { ... }
}
2 Likes

I don't think such a direction will be pursued because Swift generally prefers explicit conversions.

Even implicit Array casting has been criticized, due to the potential subtle performance penalties of casting. Take the following example (on 64 bit):

MemoryLayout<Any>.stride // 32
MemoryLayout<Int>.stride // 8

Even a simple cast from [Int] to [Any] will go over each element and allocate new storage, performing a potentially expensive map operation with very innocent-looking syntax. While some optimizations could make this operation less costly, it is still generally avoided.

That's why Swift prefers opening existentials to operate directly on the underlying value. When calling type(of: c).proxyOfAnyP(), the proxyOfAnyP method will operate on the underlying type and not the existential, because self must conform to P. This feature will be expanded to functions accepting certain existentials with SE-0352, which you can use in your example.

func main() {
    let c: any P = C()

    let anyTypeP = type(of: c)
    withOpenedAnyP(type: anyTypeP)
}

func withOpenedAnyP<Value: P>(type: Value.Type) {
    // COMPILES
    let a: [Value] = Value.arrayOfSelf()
    
    // COMPILES
    let b: Proxy<Value> = Value.proxyOfSelf()
}
1 Like

Awesome, that all makes sense. Thanks so much for your explanations and insight!

1 Like