swift generics overriding dispatch specialization

La función genérica especializada incorrecta se llama en Swift 3 desde una llamada indirecta



generics overriding (1)

Tengo un código que sigue el diseño general de:

protocol DispatchType {} class DispatchType1: DispatchType {} class DispatchType2: DispatchType {} func doBar<D:DispatchType>(value:D) { print("general function called") } func doBar(value:DispatchType1) { print("DispatchType1 called") } func doBar(value:DispatchType2) { print("DispatchType2 called") }

donde en realidad DispatchType es en realidad un almacenamiento de fondo. Las funciones doBar son métodos optimizados que dependen del tipo de almacenamiento correcto. Todo funciona bien si lo hago:

let d1 = DispatchType1() let d2 = DispatchType2() doBar(value: d1) // "DispatchType1 called" doBar(value: d2) // "DispatchType2 called"

Sin embargo, si hago una función que llama a doBar :

func test<D:DispatchType>(value:D) { doBar(value: value) }

e intento un patrón de llamada similar, obtengo:

test(value: d1) // "general function called" test(value: d2) // "general function called"

Esto parece algo que Swift debería poder manejar, ya que debería poder determinar en tiempo de compilación las restricciones de tipo. Solo como una prueba rápida, también intenté escribir doBar como:

func doBar<D:DispatchType>(value:D) where D:DispatchType1 { print("DispatchType1 called") } func doBar<D:DispatchType>(value:D) where D:DispatchType2 { print("DispatchType2 called") }

pero obtén los mismos resultados.

¿Alguna idea de si este es el comportamiento correcto de Swift, y si es así, una buena manera de evitar este comportamiento?

Edición 1 : Ejemplo de por qué estaba tratando de evitar el uso de protocolos. Supongamos que tengo el código (muy simplificado de mi código real):

protocol Storage { // ... } class Tensor<S:Storage> { // ... }

Para la clase Tensor , tengo un conjunto básico de operaciones que se pueden realizar en el Tensor . Sin embargo, las operaciones mismas cambiarán su comportamiento en función del almacenamiento. Actualmente lo logro con:

func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }

Si bien puedo ponerlos en la clase Tensor y usar extensiones:

extension Tensor where S:CBlasStorage { func dot(_ tensor:Tensor<S>) -> Tensor<S> { // ... } }

Esto tiene algunos efectos secundarios que no me gustan:

  1. Creo que dot(lhs, rhs) es preferible a lhs.dot(rhs) . Se pueden escribir funciones de conveniencia para evitar esto, pero eso creará una gran explosión de código.

  2. Esto hará que la clase Tensor vuelva monolítica. Realmente prefiero que contenga la cantidad mínima de código necesaria y expanda su funcionalidad mediante funciones auxiliares.

  3. En relación con (2), esto significa que cualquiera que quiera agregar una nueva funcionalidad tendrá que tocar la clase base, lo que considero un mal diseño.

Edición 2 : una alternativa es que las cosas funcionen de manera esperada si usa restricciones para todo:

func test<D:DispatchType>(value:D) where D:DispatchType1 { doBar(value: value) } func test<D:DispatchType>(value:D) where D:DispatchType2 { doBar(value: value) }

hará que se doBar al doBar correcto. Esto tampoco es ideal, ya que hará que se escriba mucho código adicional, pero al menos me permite mantener mi diseño actual.

Edición 3 : Encontré documentación que muestra el uso de palabras clave static con genéricos. Esto ayuda al menos con el punto (1):

class Tensor<S:Storage> { // ... static func cos(_ tensor:Tensor<S>) -> Tensor<S> { // ... } }

te permite escribir:

let result = Tensor.cos(value)

y es compatible con la sobrecarga del operador:

let result = value1 + value2

tiene la verbosidad añadida del Tensor requerido. Esto puede mejorar un poco con:

typealias T<S:Storage> = Tensor<S>


De hecho, este es el comportamiento correcto, ya que la resolución de sobrecarga tiene lugar en tiempo de compilación (sería una operación bastante costosa en tiempo de ejecución). Por lo tanto, desde dentro de la test(value:) , lo único que el compilador sabe sobre el value es que es de algún tipo que se ajusta a DispatchType ; por lo tanto, la única sobrecarga a la que puede enviar es func doBar<D : DispatchType>(value: D) .

Las cosas serían diferentes si el compilador especializara siempre las funciones genéricas, porque entonces una implementación especializada de test(value:) conocería el tipo concreto de value y, por lo tanto, podría elegir la sobrecarga adecuada. Sin embargo, la especialización de las funciones genéricas es actualmente solo una optimización (ya que sin la inclusión en línea, puede agregar una hinchazón significativa a su código), por lo que esto no cambia el comportamiento observado.

Una solución para permitir el polimorfismo es aprovechar la tabla de testigos del protocolo (vea esta gran charla de WWDC sobre ellos) agregando doBar() como un requisito del protocolo e implementando implementaciones especializadas en las clases respectivas que se ajustan al protocolo , con la implementación general como parte de la extensión del protocolo.

Esto permitirá el despacho dinámico de doBar() , permitiendo así que se doBar() desde test(value:) y se llame a la implementación correcta.

protocol DispatchType { func doBar() } extension DispatchType { func doBar() { print("general function called") } } class DispatchType1: DispatchType { func doBar() { print("DispatchType1 called") } } class DispatchType2: DispatchType { func doBar() { print("DispatchType2 called") } } func test<D : DispatchType>(value: D) { value.doBar() } let d1 = DispatchType1() let d2 = DispatchType2() test(value: d1) // "DispatchType1 called" test(value: d2) // "DispatchType2 called"