def - scala implicit
¿Cuál es la diferencia entre autotipos y subclases de rasgos? (11)
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 no extienda también B
" .
Por otro lado, lo siguiente:
trait B
trait A extends B
dice que "cualquier clase (concreta o abstracta) que se mezcle en A
tambié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?
Empecemos por 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 a primera vista, ya que puede anular los tipos de sí mismo 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
Si bien, si reemplaza a un miembro de tipo propio, perderá el acceso al miembro original, al que aún se puede acceder mediante la herencia de uso superior. 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 la torta, pero me parece 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 puedes hacer con tipos propios que no puedes 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 rutas, ya que tenemos rasgos / clases de constructores de tipos. Como por desgracia
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 se debe enfatizar 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 la instanciación). Sin embargo, val geomR
debe inicializar antes de usarlo heredando rasgos. Así que necesitamos una clase para imponer la inicialización previa de geomR
. La clase ScnVista
puede ser heredada por múltiples rasgos ortogonales que pueden ser heredados. El uso de múltiples parámetros de tipo (genéricos) ofrece una forma alternativa de modularidad.
La Sección 2.3 "Anotaciones de Autotipo" de las abstracciones de componentes escalables del papel Scala original de Martin Odersky en realidad explica muy bien el propósito del autotipo más allá de la composición mixta: proporcionar una forma alternativa de asociar una clase con un tipo abstracto.
El ejemplo dado en el documento era como 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;
}
}
Los tipos propios permiten definir dependencias cíclicas. Por ejemplo, puedes lograr esto:
trait A { self: B => }
trait B { self: A => }
La herencia de 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, consulte la sección 33.5 (Creación del capítulo de la IU de la hoja de cálculo) donde se menciona:
En el ejemplo de la hoja de cálculo, el modelo de clase se hereda de Evaluator y, por lo tanto, obtiene acceso a su método de evaluación. Para ir al otro lado, la clase Evaluador define su tipo propio para ser Modelo, así:
package org.stairwaybook.scells
trait Evaluator { this: Model => ...
Espero que esto ayude.
Otra cosa que no se ha mencionado: debido a que los tipos de sí mismos no forman parte de la jerarquía de la clase requerida, se pueden excluir de la coincidencia de patrones, especialmente cuando se hace una comparación exhaustiva 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
Se utiliza principalmente para 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, incluyendo presentaciones y videos. Por ahora, aquí hay un enlace a otra pregunta .
Ahora, en cuanto a cuál es la diferencia entre un tipo propio y la extensión de un rasgo, eso es simple. Si dices que B extends A
, entonces B
es una A
Cuando usas auto-tipos, B
requiere una A
Hay dos requisitos específicos que se crean con auto-tipos:
- Si
B
es extendida, entonces se requiere que mezcle unaA
- Cuando una clase concreta finalmente extiende / 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
| }
<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 Tweeter
fuera una subclase de User
, no habría ningún error. En el código anterior, requerimos un User
siempre que se utiliza Tweeter
, sin embargo, no se proporcionó un User
a Wrong
, por lo que obtuvimos un error. Ahora, con el código anterior aún en 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 el Right
, se cumple el requisito de mezclar un User
. Sin embargo, el segundo requisito mencionado anteriormente no se cumple: la carga de implementación del User
sigue siendo para las clases / características que se extienden a la Right
.
Con RightAgain
se cumplen ambos requisitos. Se proporciona un User
y una implementación de User
.
Para más casos prácticos de uso, ¡vea los enlaces al comienzo de esta respuesta! Pero, con suerte ahora lo entiendes.
TL; DR resumen de las otras respuestas:
Los tipos que extiendes están expuestos a los tipos heredados, pero los autotipos no están
por ejemplo:
class Cow { this: FourStomachs }
permite usar métodos solo disponibles para rumiantes, comodigestGrass
. Los rasgos que extienden a la vaca, sin embargo, no tendrán tales privilegios. Por otro lado, laclass Cow extends FourStomachs
expondrádigestGrass
a cualquiera queextends Cow
.los autotipos permiten dependencias cíclicas, extendiendo otros tipos no
Un tipo propio le permite especificar qué tipos pueden mezclarse en un rasgo. Por ejemplo, si tiene un rasgo con un tipo de Closeable
, entonces ese rasgo sabe que las únicas cosas que pueden mezclarlo deben implementar la interfaz de Closeable
.
Una diferencia adicional es que los tipos propios pueden especificar tipos que no son de clase. Por ejemplo
trait Foo{
this: { def close:Unit} =>
...
}
El tipo propio aquí es un tipo estructural. El efecto es decir que cualquier cosa que se mezcle en Foo debe implementar una unidad de retorno de "cierre" sin argumentos. Esto permite mezclas seguras para la tipificación de patos.
en el primer caso, un subtrito o subclase de B puede mezclarse con cualquier uso A. Entonces, B puede ser un rasgo abstracto.
Actualización: la principal diferencia es que los tipos propios pueden depender de varias 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 la combinación de Employee
solo a cualquier cosa que sea una subclase de Person
y Expense
. Por supuesto, esto solo es significativo si Expense
Expansión de Person
o viceversa. El punto es que el uso de tipos Employee
pueden ser independientes de la jerarquía de las clases de las que depende. No importa lo que extienda qué: si cambia la jerarquía de Expense
frente a Person
, no tiene que modificar Employee
.
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
}