¿Cuál es la diferencia entre auto-tipos y herencia de rasgos en Scala?

9

Cuando buscas en Google, surgen muchas respuestas para este tema. Sin embargo, no creo que ninguno de ellos haga un buen trabajo al ilustrar la diferencia entre estas dos características. Así que me gustaría probar una vez más, específicamente ...

¿Qué es algo que se puede hacer con auto-tipos y no con herencia, y viceversa?

Para mí, debería haber alguna diferencia física cuantificable entre los dos, de lo contrario son nominalmente diferentes.

Si el rasgo A extiende a B o al tipo B, ¿no ilustran ambos que ser B es un requisito? ¿Dónde está la diferencia?

Mark Canlas
fuente
Tengo cuidado con los términos que ha establecido en la recompensa. Por un lado, defina la diferencia "física", dado que todo esto es software. Más allá de eso, para cualquier objeto compuesto que cree con mixins, probablemente pueda crear algo aproximado en función de la herencia, si define la función únicamente en términos de los métodos visibles. Donde diferirán es en extensibilidad, flexibilidad y compostibilidad.
itsbruce
Si tiene una variedad de placas de acero de diferentes tamaños, puede atornillarlas para formar una caja o puede soldarlas. Desde una perspectiva estrecha, estas serían equivalentes en funcionalidad, si ignora el hecho de que una se puede reconfigurar o extender fácilmente y la otra no. Tengo la sensación de que va a argumentar que son equivalentes, aunque me alegraría que me demuestren lo contrario si dijera más sobre sus criterios.
itsbruce
Estoy más que familiarizado con lo que está diciendo en general, pero todavía no entiendo cuál es la diferencia en este caso particular. ¿Podría proporcionar algunos ejemplos de código que muestren que un método es más extensible y flexible que el otro? * Código base con extensión * Código base con tipos propios * Característica agregada al estilo de extensión * Característica agregada al estilo de tipo automático
Mark Canlas
OK, creo que puedo intentarlo antes de que se acabe la recompensa;)
itsbruce

Respuestas:

11

Si el rasgo A extiende a B, entonces mezclar en A le da exactamente B más lo que A agrega o extiende. Por el contrario, si el rasgo A tiene una autorreferencia que se escribe explícitamente como B, entonces la clase padre última también debe mezclar B o un tipo descendiente de B (y mezclarlo primero , lo cual es importante).

Esa es la diferencia más importante. En el primer caso, el tipo preciso de B se cristaliza en el punto A lo extiende. En el segundo, el diseñador de la clase principal decide qué versión de B se utiliza, en el punto donde se compone la clase principal.

Otra diferencia es donde A y B proporcionan métodos del mismo nombre. Donde A extiende a B, el método de A anula a B. Cuando A se mezcla después de B, el método de A simplemente gana.

La autorreferencia mecanografiada te da mucha más libertad; El acoplamiento entre A y B está suelto.

ACTUALIZAR:

Como no tiene claro el beneficio de estas diferencias ...

Si usa la herencia directa, entonces crea el rasgo A que es B + A. Has establecido la relación en piedra.

Si usa una autorreferencia escrita, cualquiera que quiera usar su rasgo A en la clase C podría

  • Mezcle B y luego A en C.
  • Mezcle un subtipo de B y luego A en C.
  • Mezcle A en C, donde C es una subclase de B.

Y este no es el límite de sus opciones, dada la forma en que Scala le permite instanciar un rasgo directamente con un bloque de código como su constructor.

En cuanto a la diferencia entre el método ganador de A , porque A se mezcla en último lugar, en comparación con A que extiende B, considere esto ...

Cuando se mezcla una secuencia de rasgos, cada vez que foo()se invoca el método , el compilador va al último rasgo mezclado para buscar foo(), luego (si no se encuentra), atraviesa la secuencia hacia la izquierda hasta que encuentra un rasgo que implementa foo()y utiliza ese. A también tiene la opción de llamar super.foo(), que también atraviesa la secuencia hacia la izquierda hasta que encuentra una implementación, y así sucesivamente.

Entonces, si A tiene una autorreferencia escrita a B y el escritor de A sabe que B implementa foo(), A puede llamar super.foo()sabiendo que si nada más proporciona foo(), B lo hará. Sin embargo, el creador de la clase C tiene la opción de eliminar cualquier otro rasgo en el que se implemente foo(), y A lo obtendrá en su lugar.

De nuevo, esto es mucho más potente y menos limitante que A que se extiende B y llamando directamente a la versión de B de foo().

itsbruce
fuente
¿Cuál es la diferencia funcional entre un ganador frente a una anulación? ¿Recibo A en ambos casos a través de diferentes mecanismos? Y en su primer ejemplo ... En su primer párrafo, ¿por qué no hacer que el rasgo A extienda SuperOfB? Parece que siempre podríamos remodelar el problema usando cualquiera de los mecanismos. Supongo que no veo un caso de uso en el que esto no sea posible. O estoy asumiendo demasiadas cosas.
Mark Canlas
¿Por qué querrías que A extienda una subclase de B, si B define lo que necesitas? La autorreferencia obliga a B (o una subclase) a estar presente, pero le da al desarrollador la opción? Pueden mezclar algo que escribieron después de que escribiste el rasgo A, siempre que se extienda B. ¿Por qué restringirlos solo a lo que estaba disponible cuando escribiste el rasgo A?
itsbruce
Actualizado para hacer la diferencia súper clara.
itsbruce
@itsbruce ¿hay alguna diferencia conceptual? ¿IS-A versus HAS-A?
Jas
@Jas En el contexto de la relación entre los rasgos A y B , la herencia es IS-A, mientras que la autorreferencia escrita da HAS-A (una relación de composición). Para la clase en la que se mezclan los rasgos, el resultado es IS-A , independientemente.
itsbruce
0

Tengo un código que ilustra algunas de las diferencias en la visibilidad de wrt y las implementaciones "predeterminadas" cuando se extiende frente a la configuración de un auto-tipo. No ilustra ninguna de las partes discutidas ya sobre cómo se resuelven las colisiones de nombres reales, sino que se centra en lo que es posible y no es posible hacer.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Una diferencia importante de la OMI es que A1no expone que es necesario Bpara nada que simplemente lo vea A1(como se ilustra en las partes mejoradas). El único código que realmente verá que Bse utiliza una especialización particular es el código que sabe explícitamente sobre el tipo compuesto (como Think*{X,Y}).

Otro punto es que A2(con extensión) realmente se usará Bsi no se especifica nada más, mientras que A1(auto-tipo) no dice que se usará a Bmenos que se anule, se debe dar explícitamente una B concreta cuando se instancian los objetos.

Rouzbeh Delavari
fuente