¿Cuál es la diferencia entre los tipos propios y las subclases de rasgos?

387

Un auto-tipo para un rasgo A:

trait B
trait A { this: B => }

dice que " Ano se puede mezclar en una clase concreta que no se extienda B" .

Por otro lado, lo siguiente:

trait B
trait A extends B

dice que "cualquier clase (concreta o abstracta) que se mezcle Atambién se mezclará en B" .

¿Estas dos afirmaciones no significan lo mismo? El auto-tipo parece servir solo para crear la posibilidad de un simple error en tiempo de compilación.

¿Qué me estoy perdiendo?

Dave
fuente
De hecho, estoy interesado aquí en las diferencias entre los tipos propios y las subclases en rasgos. Conozco algunos de los usos comunes de los auto-tipos; Simplemente no puedo encontrar una razón por la cual no se harían más claramente de la misma manera con el subtipo.
Dave
32
Uno puede usar parámetros de tipo dentro de los auto-tipos: trait A[Self] {this: Self => }es legal, trait A[Self] extends Selfno lo es.
Blaisorblade
3
Un auto tipo también puede ser una clase, pero un rasgo no puede heredar de una clase.
cvogt
10
@cvogt: un rasgo puede heredar de una clase (al menos a partir de 2.10): pastebin.com/zShvr8LX
Erik Kaplun
1
@Blaisorblade: ¿no es eso algo que podría resolverse mediante un rediseño de un lenguaje pequeño, y no es una limitación fundamental? (al menos desde el punto de vista de la pregunta)
Erik Kaplun

Respuestas:

273

Se utiliza principalmente para la inyección de dependencia , como en el patrón de pastel. Existe un gran artículo que cubre muchas formas diferentes de inyección de dependencia en Scala, incluido el Patrón de pastel. Si buscas en Google "Cake Pattern and Scala", obtendrás muchos enlaces, incluidas presentaciones y videos. Por ahora, aquí hay un enlace a otra pregunta .

Ahora, en cuanto a cuál es la diferencia entre un auto tipo y extender un rasgo, eso es simple. Si dices B extends A, entonces B es un A. Cuando usas auto-tipos, B requiere un A. Hay dos requisitos específicos que se crean con autotipos:

  1. Si Bse prolonga, entonces usted está obligado a mezclar en una A.
  2. Cuando una clase concreta finalmente extiende / mezcla estos rasgos, se debe implementar alguna clase / rasgo A.

Considere los siguientes ejemplos:

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Si Tweeterfuera una subclase de User, no habría error. En el código anterior, que requiere una Usercada vez que Tweeterse utiliza, sin embargo, un Userno se proporcionó a Wrong, así que conseguimos un error. Ahora, con el código anterior todavía en el alcance, considere:

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

Con Right, Userse cumple el requisito de mezclar a . Sin embargo, el segundo requisito mencionado anteriormente no se cumple: la carga de la implementación Usersigue siendo para las clases / rasgos que se extienden Right.

Con RightAgainambos requisitos se cumplen. A Usery una implementación de Userse proporcionan.

Para casos de uso más prácticos, consulte los enlaces al comienzo de esta respuesta. Pero, con suerte ahora lo entiendes.

Daniel C. Sobral
fuente
3
Gracias. El patrón de pastel es el 90% de lo que quiero decir por qué hablo sobre el bombo en torno a los auto-tipos ... es donde vi por primera vez el tema. El ejemplo de Jonas Boner es genial porque subraya el punto de mi pregunta. Si cambiara los tipos de uno mismo en su ejemplo de calentador para que fueran subtraits, ¿cuál sería la diferencia (además del error que obtiene al definir el ComponentRegistry si no combina las cosas correctas?
Dave
29
@Dave: ¿Quieres decir como trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Eso provocaría WarmerComponentImpltener esas interfaces. Estarían disponibles para cualquier cosa que se extienda WarmerComponentImpl, lo cual es claramente incorrecto, ya que no es un SensorDeviceComponent, ni un OnOffDeviceComponent. Como tipo automático, estas dependencias están disponibles exclusivamente para WarmerComponentImpl. A Listpodría usarse como un Arrayy viceversa. Pero simplemente no son lo mismo.
Daniel C. Sobral
10
Gracias Daniel Esta es probablemente la mayor distinción que estaba buscando. El problema práctico es que el uso de subclases filtrará funcionalidades en su interfaz que no tiene intención. Es el resultado de la violación de la regla más teórica de "es parte de una" para los rasgos. Los auto-tipos expresan una relación "usa-a" entre las partes.
Dave
11
@ Rodney No, no debería. De hecho, usar thiscon tipos propios es algo que menosprecio, ya que no tiene ninguna razón para ocultar el original this.
Daniel C. Sobral
99
@opensas Intenta self: Dep1 with Dep2 =>.
Daniel C. Sobral
156

Los tipos propios le permiten definir dependencias cíclicas. Por ejemplo, puedes lograr esto:

trait A { self: B => }
trait B { self: A => }

El uso de la herencia extendsno lo permite. Tratar:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

En el libro de Odersky, mire la sección 33.5 (Creación del capítulo de la UI de la hoja de cálculo) donde menciona:

En el ejemplo de la hoja de cálculo, la clase Modelo hereda del Evaluador y, por lo tanto, obtiene acceso a su método de evaluación. Para ir hacia otro lado, la clase Evaluador define su auto tipo como Modelo, de esta manera:

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

Espero que esto ayude.

Mushtaq Ahmed
fuente
3
No había considerado este escenario. Es el primer ejemplo de algo que he visto que no es lo mismo que un auto-tipo como lo es con una subclase. Sin embargo, parece una especie de casey de borde y, lo que es más importante, parece una mala idea (¡generalmente me salgo de mi camino para NO definir dependencias cíclicas!). ¿Considera que esta es la distinción más importante?
Dave
44
Creo que sí. No veo ninguna otra razón por la que preferiría auto-tipos para ampliar la cláusula. Los tipos automáticos son detallados, no se heredan (por lo que debe agregar tipos automáticos a todos los subtipos como un ritual) y solo puede ver los miembros pero no puede anularlos. Conozco bien el patrón de Cake y muchas publicaciones que mencionan los tipos propios para DI. Pero de alguna manera no estoy convencido. Había creado una aplicación de muestra aquí hace mucho tiempo ( bitbucket.org/mushtaq/scala-di ). Mire específicamente a la carpeta / src / configs. Logré DI para reemplazar configuraciones complejas de Spring sin auto-tipos.
Mushtaq Ahmed
Mushtaq, estamos de acuerdo. Creo que la declaración de Daniel acerca de no exponer la funcionalidad no intencional es importante, pero, como lo expresas, hay una vista espejo de esta 'característica' ... que no puedes anular la funcionalidad o usarla en futuras subclases. Esto me dice claramente cuándo el diseño requerirá uno sobre el otro. Evitaré los tipos automáticos hasta que encuentre una necesidad genuina, es decir, si empiezo a usar objetos como módulos, como señala Daniel. Estoy autoconectando dependencias con parámetros implícitos y un objeto de arranque directo. Me gusta la simplicidad.
Dave
@ DanielC.Sobral puede ser gracias a tu comentario, pero en este momento tiene más votos a favor que tu respuesta. Votando a ambos :)
rintcius
¿Por qué no simplemente crear un rasgo AB? Como los rasgos A y B siempre deben combinarse en cualquier clase final, ¿por qué separarlos en primer lugar?
Rich Oliver el
56

Una diferencia adicional es que los autotipos pueden especificar tipos que no son de clase. Por ejemplo

trait Foo{
   this: { def close:Unit} => 
   ...
}

El auto tipo aquí es un tipo estructural. El efecto es decir que cualquier cosa que se mezcle en Foo debe implementar una unidad de retorno del método "cerrar" sin argumentos. Esto permite mezclas seguras para la tipificación de patos.

Dave Griffith
fuente
41
En realidad, también puede usar la herencia con tipos estructurales: la clase abstracta A se extiende {def close: Unit}
Adrian
12
Creo que la tipificación estructural está usando la reflexión, así que úsela solo cuando no haya otra opción ...
Eran Medan
@ Adrian, creo que tu comentario es incorrecto. `clase abstracta A se extiende {def close: Unit}` es solo una clase abstracta con superclase de objetos. es solo una sintaxis permisiva de Scala para expresiones sin sentido. Puede `clase X extiende {def f = 1}; nuevo X (). f` por ejemplo
Alexey
1
@Alexey No veo por qué tu ejemplo (o el mío) no tiene sentido.
Adrian
1
@Adrian, abstract class A extends {def close:Unit}es equivalente a abstract class A {def close:Unit}. Por lo tanto, no involucra tipos estructurales.
Alexey
13

La sección 2.3 "Anotaciones de autotipo" del papel Scala original de Martin Odersky. Las abstracciones de componentes escalables en realidad explican muy bien el propósito del autotipo más allá de la composición mixin: proporcionar una forma alternativa de asociar una clase con un tipo abstracto.

El ejemplo dado en el documento fue el siguiente, y no parece tener un corresponsal de subclase elegante:

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}
lcn
fuente
Para aquellos que se preguntan por qué las subclases no resolverán esto, la Sección 2.3 también dice esto: “Cada uno de los operandos de una composición mixina C_0 con ... con C_n, debe referirse a una clase. El mecanismo de composición mixina no permite que ningún C_i se refiera a un tipo abstracto. Esta restricción hace posible verificar estáticamente las ambigüedades y anular los conflictos en el punto donde se compone una clase ".
Luke Maurer
12

Otra cosa que no se ha mencionado: debido a que los autotipos no son parte de la jerarquía de la clase requerida, se pueden excluir de la coincidencia de patrones, especialmente cuando se compara exhaustivamente con una jerarquía sellada. Esto es conveniente cuando desea modelar comportamientos ortogonales como:

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive
Bruno Bieth
fuente
10

TL; DR resumen de las otras respuestas:

  • Los tipos que extiende están expuestos a los tipos heredados, pero los autotipos no

    Por ejemplo: le class Cow { this: FourStomachs }permite utilizar métodos solo disponibles para rumiantes, como digestGrass. Sin embargo, los rasgos que extienden Cow no tendrán tales privilegios. Por otro lado, class Cow extends FourStomachsexpondrá digestGrassa quien sea extends Cow .

  • los auto-tipos permiten dependencias cíclicas, extender otros tipos no

jazmit
fuente
9

Comencemos con la dependencia cíclica.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

Sin embargo, la modularidad de esta solución no es tan buena como podría parecer en un principio, ya que puede anular los tipos propios de esta manera:

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

Sin embargo, si anula un miembro de tipo automático, pierde el acceso al miembro original, al que aún se puede acceder a través de super usando la herencia. Entonces, lo que realmente se gana con el uso de la herencia es:

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

Ahora no puedo afirmar que entiendo todas las sutilezas del patrón de pastel, pero me sorprende que el método principal para imponer la modularidad es a través de la composición en lugar de la herencia o los tipos propios.

La versión de herencia es más corta, pero la razón principal por la que prefiero la herencia sobre los tipos propios es que me resulta mucho más difícil obtener el orden de inicialización correcto con los tipos propios. Sin embargo, hay algunas cosas que puede hacer con los tipos propios que no puede hacer con la herencia. Los tipos propios pueden usar un tipo mientras que la herencia requiere un rasgo o una clase como en:

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

Incluso puedes hacer:

trait TypeBuster
{ this: Int with String => }

Aunque nunca podrás instanciarlo. No veo ninguna razón absoluta para no poder heredar de un tipo, pero ciertamente creo que sería útil tener clases y rasgos de constructor de ruta ya que tenemos rasgos / clases de constructor de tipo. Como lamentablemente

trait InnerA extends Outer#Inner //Doesn't compile

Tenemos esto:

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

O esto:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

Un punto que debería empatizarse más es que los rasgos pueden extender las clases. Gracias a David Maclver por señalar esto. Aquí hay un ejemplo de mi propio código:

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBasehereda de la clase Swing Frame, por lo que podría usarse como un tipo automático y luego mezclarse al final (en la instanciación). Sin embargo, val geomRdebe inicializarse antes de que se use heredando rasgos. Por lo tanto, necesitamos una clase para imponer la inicialización previa de geomR. La clase ScnVistapuede ser heredada de múltiples rasgos ortogonales de los cuales ellos mismos pueden ser heredados. El uso de múltiples parámetros de tipo (genéricos) ofrece una forma alternativa de modularidad.

Rich Oliver
fuente
7
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}
Oleg Galako
fuente
4

Un tipo automático le permite especificar qué tipos se pueden mezclar en un rasgo. Por ejemplo, si tiene un rasgo con un tipo automático Closeable, ese rasgo sabe que las únicas cosas que pueden mezclarlo deben implementar la Closeableinterfaz.

kikibobo
fuente
3
@Blaisorblade: Me pregunto si es posible que hayas leído mal la respuesta de kikibobo: el tipo propio de un rasgo realmente te permite restringir los tipos que pueden mezclarlo, y eso es parte de su utilidad. Por ejemplo, si definimos trait A { self:B => ... }, una declaración X with Asolo es válida si X extiende a B. Sí, puede decir X with A with Q, donde Q no extiende a B, pero creo que el punto de kikibobo es que X está tan restringido. ¿O me perdí algo?
AmigoNico
1
Gracias tienes razon. Mi voto fue bloqueado, pero por suerte pude editar la respuesta y luego cambiar mi voto.
Blaisorblade
1

Actualización: una diferencia principal es que los auto-tipos pueden depender de múltiples clases (admito que es un caso de esquina). Por ejemplo, puedes tener

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

Esto permite agregar el Employeemixin a cualquier cosa que sea una subclase de Persony Expense. Por supuesto, esto solo tiene sentido si se Expenseextiende Persono viceversa. El punto es que el uso de auto-tipos Employeepuede ser independiente de la jerarquía de las clases de las que depende. No le importa qué extiende qué: si cambia la jerarquía de Expensevs Person, no tiene que modificar Employee.

Petr Pudlák
fuente
El empleado no necesita ser una clase para descender de persona. Los rasgos pueden extender las clases. Si el rasgo de Empleado extendiera Persona en lugar de usar un tipo propio, el ejemplo aún funcionaría. Encuentro su ejemplo interesante, pero no parece ilustrar un caso de uso para los tipos propios.
Morgan Creighton el
@MorganCreighton Bastante justo, no sabía que los rasgos pueden extender las clases. Lo pensaré si puedo encontrar un mejor ejemplo.
Petr Pudlák
Sí, es una característica de lenguaje sorprendente. Si el rasgo Empleado extendió a la Persona de clase, entonces cualquier clase que finalmente se "marchitó" El Empleado también tendría que extender a la Persona. Pero esa restricción aún está presente si el Empleado usó un tipo automático en lugar de extender Persona. Saludos, Petr!
Morgan Creighton
1
No veo por qué "esto solo tiene sentido si el gasto extiende a la persona o viceversa".
Robin Green
0

en el primer caso, una subcaracter o subclase de B se puede mezclar con cualquier uso de A. Entonces B puede ser una característica abstracta.

IttayD
fuente
No, B puede ser (y de hecho es) un "rasgo abstracto" en ambos casos. Entonces no hay diferencia desde esa perspectiva.
Robin Green