Static protocol function dispatch surprise/confusion

I've found that when a static protocol method in turn calls a protocol instance method, the version of the instance method that gets called is less 'specialized' which is confusing me. Moreover, static protocol operator dispatch behaves differently than static protocol method dispatch, which is also confusing. Probably easier to explain with the following example:


infix operator •: MultiplicationPrecedence

protocol RectangularNumericArray {
  
  associatedtype Element:Numeric
  
  init()
  
  static func •(lhs:Self, rhs:Self) -> Self
  
  static func innerProduct(of lhs:Self, with rhs:Self) -> Self
  
  func innerProduct(with rhs:Self, transposingRhs:Bool) -> Self
  
}


extension RectangularNumericArray {

  static func •(lhs:Self, rhs:Self) -> Self {
    print("slow default static (operator) method impl")
    return lhs.innerProduct(with: rhs, transposingRhs: false)
  }
  
  static func innerProduct(of lhs:Self, with rhs:Self) -> Self {
    print("slow default static method impl")
    return lhs.innerProduct(with: rhs, transposingRhs: false)
  }
  
  func innerProduct(with rhs:Self, transposingRhs:Bool) -> Self{
    print("slow default instance method impl")
    return Self()
  }
  
}

extension RectangularNumericArray where Element == Float32 {
  
  static func •(_ lhs:Self, _ rhs:Self) -> Self {
    print("fast specialized static (operator) method impl")
    return lhs.innerProduct(with: rhs, transposingRhs:false)
  }
    
  static func innerProduct(of lhs:Self, with rhs:Self) -> Self {
    print("fast specialized static method impl")
    return lhs.innerProduct(with: rhs, transposingRhs: false)
  }

  func innerProduct(with rhs:Self, transposingRhs:Bool) -> Self {
    print("fast specialized instance method impl")
    return Self()
  }

}

struct Matrix<T:Numeric> {
}

extension Matrix : RectangularNumericArray{
  typealias Element = T
}

let M1 = Matrix<Float32>()
let M2 = Matrix<Float32>()

let _ = M1 • M2
print("-------------------------")
let _ = Matrix<Float32>.innerProduct(of: M1, with: M2)
print("-------------------------")
let _ = M1.innerProduct(with: M2, transposingRhs: false)

I would expect that the fast specialized implementation would get called in all cases, because I'm only ever using the appropriately specialized types. What I actually get is the following:

slow default static (operator) method impl
slow default instance method impl
-------------------------
fast specialized static method impl
slow default instance method impl
-------------------------
fast specialized instance method impl
Program ended with exit code: 0

This is with Xcode 13.3 beta 2.

Thanks to anyone who can help me understand,
Matt

In Swift, a type conforms to a protocol in exactly one way.

At the point where a type conforms to a protocol, the compiler identifies the implementations of all the protocol requirements for that type.

Those implementations are used whenever a requirement of the protocol is called on that type.

• • •

Your Matrix type conforms to the RectangularNumericArray protocol in exactly one way.

At the point where Matrix conforms to RectangularNumericArray, the only constraint on the generic parameter T is that T: Numeric.

The conformance specifies that Element = T, so the only constraint on Element is that Element: Numeric.

Therefore, when the compiler identifies the implementations that satisfies the requirements of the protocol for Matrix, the only candidates available are the default implementations from the unconstrained protocol extension.

It is not possible to use the implementations from the constrained protocol extension where Element == Float32, because that constraint is not satisfied at the point where Matrix conforms to the protocol.

1 Like

I understand. So for the instance method, there is some runtime dispatch happening, and for the static methods, your explanation clarifies why that can't happen.

Thanks!
Matt

No, all the function calls in your example are resolved statically at compilation time.

1 Like

Ok. I'm still a bit confused then. Why is the instance method able to run the more specialized implementation, if it too is being resolved at compile time?

In other words why is the M1.innerProduct(with: M2, transposingRhs: false) call running the Float32 specialized version of the method?

Because it’s a direct call on an instance, which does not involve the protocol at all.

Thank you.