implicitly def conversion scala traits self-type

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:

  1. Si B es extendida, entonces se requiere que mezcle una A
  2. 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, como digestGrass . Los rasgos que extienden a la vaca, sin embargo, no tendrán tales privilegios. Por otro lado, la class Cow extends FourStomachs expondrá digestGrass a cualquiera que extends 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 }