¿Cuál es la diferencia entre los self-types y las subclases de rasgos?

Un auto-tipo para un rasgo A :

 trait B trait A { this: B => } 

dice que A no se puede mezclar en una clase concreta que tampoco se extiende B .

Por otro lado, lo siguiente:

 trait B trait A extends B 

dice que “cualquier clase de mezcla (concreta o abstracta) en A también se mezclará en B” .

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

¿Qué me estoy perdiendo?

Se usa predominantemente para la Inyección de Dependencia , como en el Patrón de Pastel . Existe un gran artículo que cubre muchas formas diferentes de dependency injection en Scala, incluido el patrón de tortas. Si busca en Google “Cake Pattern and Scala”, obtendrá 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 tipo de sí mismo y extender un rasgo, eso es simple. Si dices que B extends A , entonces B es un A Cuando usa auto-tipos, B requiere una A Hay dos requisitos específicos que se crean con los propios tipos:

  1. Si B se extiende, entonces se requiere que mezcle una A
  2. Cuando una clase concreta finalmente se extiende / se mezcla -en estos rasgos, alguna clase / rasgo debe implementar 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 | } :9: error: illegal inheritance; self-type Wrong does not conform to Tweeter's selftype Tweeter with User trait Wrong extends Tweeter { ^ :10: error: not found: value name def noCanDo = name ^ 

Si Tweeter fuera una subclase de User , no habría error. En el código anterior, solicitamos un User cada vez que utilizamos Tweeter , sin embargo, no se proporcionó un User a Wrong , por lo que obtuvimos un error. Ahora, con el código anterior todavía en el scope, 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 , el requisito de mezclar un User está satisfecho. Sin embargo, el segundo requisito mencionado anteriormente no se cumple: la carga de la implementación del User aún permanece para las clases / características que se extienden a la Right .

Con RightAgain ambos requisitos están satisfechos. Se proporciona un User y una implementación de User .

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

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

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

La herencia que usa extends no permite eso. Tratar:

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

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

En el ejemplo de la hoja de cálculo, el Modelo de clase hereda de Evaluator y así obtiene acceso a su método de evaluación. Para ir por el otro lado, el Evaluador de la clase define su propio tipo como Modelo, como este:

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

Espero que esto ayude.

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

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

El tipo de self aquí es un tipo estructural. El efecto es decir que cualquier cosa que se mezcle en Foo debe implementar una unidad de retorno de método “cerrado” sin argumentos. Esto permite mezclas seguras para el tipado de patos.

La sección 2.3 “Anotaciones del tipo de sí mismo” del original Scala Component Abstractions de Martin Odersky explica en realidad el propósito del selftype más allá de la composición de mixin: proporciona una forma alternativa de asociar una clase con un tipo abstracto.

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

 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; } } 

Otra cosa que no se ha mencionado es que, debido a que los tipos propios no son parte de la jerarquía de la clase requerida, pueden excluirse de la coincidencia de patrones, especialmente cuando se hace coincidir 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 

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, ya que puede anular los tipos propios de la siguiente 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 

Aunque, si anulas un miembro de un tipo propio, pierdes el acceso al miembro original, que todavía se puede acceder a través de super usando 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 tortas, pero me sorprende que el método principal para imponer la modularidad sea 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 self-types es que me resulta mucho más complicado obtener la orden de inicialización correcta con los self-types. Sin embargo, hay algunas cosas que puedes hacer con auto tipos que no puedes hacer con la herencia. Los propios tipos 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 características / clases de constructor de tipo. Como desafortunadamente

 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 debe 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] 

ScnBase hereda de la clase Swing Frame, por lo que podría usarse como un tipo propio y luego mezclarse al final (en instanciación). Sin embargo, val geomR necesita ser inicializado antes de ser usado heredando rasgos. Entonces, necesitamos una clase para hacer cumplir la inicialización previa de geomR . La clase ScnVista puede ser heredada por múltiples rasgos ortogonales de los cuales ellos mismos pueden heredarse. El uso de parámetros de tipos múltiples (generics) ofrece una forma alternativa de modularidad.

TL; DR resumen de las otras respuestas:

  • Los tipos que extiendes están expuestos a tipos heredados, pero los auto-tipos no son

    por ejemplo: class Cow { this: FourStomachs } permite usar métodos solo disponibles para rumiantes, como digestGrass . Sin embargo, los rasgos que extienden Cow no tendrán tales privilegios. Por otro lado, la class Cow extends FourStomachs expondrá digestGrass a cualquiera que extends Cow .

  • los tipos propios permiten dependencias cíclicas, extendiendo otros tipos no

 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 } 

Un tipo propio le permite especificar qué tipos pueden mezclarse en un rasgo. Por ejemplo, si tiene un rasgo con un tipo de Closeable , ese rasgo sabe que las únicas cosas que se pueden mezclar deben implementar la interfaz de Closeable .

Actualización: Una diferencia principal es que los auto-tipos pueden depender de múltiples clases (admito que es un poco el caso de la 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 la combinación de Employee solo a cualquier cosa que sea una subclase de Person y Expense . Por supuesto, esto solo tiene sentido si Expense extiende Person o viceversa. El punto es que usar Self-types Employee puede ser independiente de la jerarquía de las clases de las que depende. No importa qué es lo que extiende qué: si cambia la jerarquía de Expense vs Person , no tiene que modificar Employee .

en el primer caso, un sub-rasgo o sub-clase de B se puede mezclar para cualquier uso A. Entonces B puede ser un rasgo abstracto.