web-dev-qa-db-fra.com

Quelle est la différence entre les types de soi et les sous-classes de traits?

Un self-type pour un trait A:

trait B
trait A { this: B => }

dit que "A ne peut pas être mélangé dans une classe concrète qui ne s'étend pas également B".

Par contre, ce qui suit:

trait B
trait A extends B

dit que "toute classe (concrète ou abstraite) mélangée dans A sera également mélangée dans B".

Ces deux déclarations ne signifient-elles pas la même chose? Le self-type semble ne servir qu'à créer la possibilité d'une simple erreur de compilation.

Qu'est-ce que je rate?

374
Dave

Il est principalement utilisé pour Injection de dépendance , comme dans le modèle de gâteau. Il existe un excellent article couvrant de nombreuses formes différentes d'injection de dépendance dans Scala, y compris le modèle de gâteau. Si vous utilisez Google "Modèle de gâteau et Scala", vous obtiendrez de nombreux liens, notamment des présentations et des vidéos. Pour l'instant, voici un lien vers ne autre question .

Maintenant, quant à la différence entre un type de soi et l’extension d’un trait, c’est simple. Si vous dites B extends A, alors B est un A. Lorsque vous utilisez des self-types, B nécessite un A. Deux exigences spécifiques sont créées avec des types individuels:

  1. Si B est étendu, il vous faut pour mélanger un A.
  2. Quand une classe concrète étend/mélange finalement ces traits, une classe/trait doit implémenter A.

Considérez les exemples suivants:

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 était une sous-classe de User, il n'y aurait pas d'erreur. Dans le code ci-dessus, nous demandions un User à chaque fois que Tweeter est utilisé, mais un User n'était pas fourni à Wrong, nous avons donc une erreur. Maintenant, avec le code ci-dessus toujours dans la portée, considérons:

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

Avec Right, il est satisfait à la nécessité de mélanger un User. Cependant, la deuxième condition mentionnée ci-dessus n'est pas satisfaite: la charge de la mise en œuvre de User subsiste pour les classes/traits qui s'étendent Right.

Avec RightAgain les deux conditions sont remplies. Un User et une implémentation de User sont fournis.

Pour des cas d'utilisation plus pratiques, veuillez consulter les liens au début de cette réponse! Mais si tout va bien maintenant vous l'obtenez.

264
Daniel C. Sobral

Les types d'individus vous permettent de définir des dépendances cycliques. Par exemple, vous pouvez réaliser ceci:

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

L'héritage avec extends ne le permet pas. Essayer:

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

Dans le livre Odersky, reportez-vous à la section 33.5 (Création d’un chapitre sur l’interface utilisateur de feuille de calcul) où elle est mentionnée:

Dans l'exemple de feuille de calcul, la classe Model hérite de Evaluator et accède ainsi à sa méthode d'évaluation. Pour l'inverse, la classe Evaluator définit son type comme étant Model, comme ceci:

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

J'espère que cela t'aides.

153
Mushtaq Ahmed

Une différence supplémentaire est que les self-types peuvent spécifier des types sans classe. Par exemple

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

Le type de self ici est un type structurel. L'effet est de dire que tout ce qui se mélange dans Foo doit implémenter une unité renvoyant la méthode "sans argument". Cela permet des mélanges sécurisés pour le typage de canards.

56
Dave Griffith

La section 2.3 "Annotations sur types distincts" du document original de Scala de Martin Odersky Abstractions de composants évolutifs explique en fait le but du type autonome bien au-delà de la composition en mix: offre un moyen alternatif d’associer une classe à un type abstrait.

L'exemple donné dans l'article était le suivant et il ne semble pas avoir de correspondant élégant dans la sous-classe:

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

Une autre chose qui n’a pas été mentionnée: les self-types ne faisant pas partie de la hiérarchie de la classe requise, ils peuvent être exclus de la correspondance de modèle, en particulier lorsque vous effectuez une correspondance exhaustive avec une hiérarchie scellée. Ceci est pratique lorsque vous souhaitez modéliser des comportements orthogonaux tels que:

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
12
Bruno Bieth

TL; DR résumé des autres réponses:

  • Les types que vous étendez sont exposés aux types hérités, mais les types propres ne le sont pas.

    exemple: class Cow { this: FourStomachs } vous permet d'utiliser des méthodes uniquement disponibles pour les ruminants, telles que digestGrass. Les traits qui prolongent Vache n'auront cependant pas de tels privilèges. D'autre part, class Cow extends FourStomachs exposera digestGrass à toute personne qui extends Cow.

  • les types permettent des dépendances cycliques, l'extension d'autres types ne

10
jazmit

Commençons par la dépendance cyclique.

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

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

Toutefois, la modularité de cette solution n’est pas aussi grande qu’elle pourrait l’être au premier abord, car vous pouvez remplacer les types de self de la manière suivante:

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

Toutefois, si vous substituez un membre d'un type self, vous perdez l'accès au membre d'origine, auquel il est toujours possible d'accéder via l'héritage super. Donc, ce qui est vraiment gagné en utilisant l'héritage, c'est:

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

Maintenant, je ne peux pas prétendre comprendre toutes les subtilités du modèle de gâteau, mais il me semble que la méthode principale pour appliquer la modularité consiste à utiliser la composition plutôt que les types d'héritage ou de soi.

La version d'héritage est plus courte, mais la principale raison pour laquelle je préfère l'héritage aux types d'individus est que je trouve qu'il est beaucoup plus difficile d'obtenir l'ordre d'initialisation correct avec les types d'individus. Cependant, vous pouvez faire certaines choses avec les types de soi que vous ne pouvez pas faire avec l'héritage. Les types auto peuvent utiliser un type alors que l'héritage nécessite un trait ou une classe comme dans:

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

Vous pouvez même faire:

trait TypeBuster
{ this: Int with String => }

Bien que vous ne puissiez jamais l'instancier. Je ne vois aucune raison absolue de ne pas pouvoir hériter d'un type, mais j'estime qu'il serait utile de disposer de classes et de traits de constructeur de chemins, tout comme nous avons des traits/classes de constructeur de types. Comme malheureusement

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

Nous avons ceci:

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 }

Ou ca:

  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 point qui devrait être plus empathisé est que les traits peuvent prolonger les classes. Merci à David Maclver pour l'avoir signalé. Voici un exemple de mon propre code:

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 hérite de la classe Swing Frame, elle pourrait donc être utilisée comme type self, puis être mélangée à la fin (lors de l'instanciation). Cependant, val geomR doit être initialisé avant d'être utilisé pour hériter des traits. Nous avons donc besoin d’une classe pour forcer l’initialisation préalable de geomR. La classe ScnVista peut ensuite être héritée de plusieurs traits orthogonaux dont ils peuvent eux-mêmes hériter. L'utilisation de plusieurs paramètres de type (génériques) offre une autre forme de modularité.

9
Rich Oliver
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
}
7
Oleg Galako

Un type auto vous permet de spécifier les types autorisés à mélanger un trait. Par exemple, si vous avez un trait avec un type de soi Closeable, alors ce trait sait que les seules choses autorisées à le mélanger, doivent implémenter l'interface Closeable.

4
kikibobo

Mise à jour: Une différence principale est que les self-types peuvent dépendre de plusieurs classes (j'admets que c'est un peu le cas). Par exemple, vous pouvez avoir

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

Cela permet d’ajouter le Employee mixin à tout ce qui est une sous-classe de Person et Expense. Bien entendu, cela n'a de sens que si Expense s'étend Person ou inversement. Le fait est que l'utilisation de self-types Employee peut être indépendante de la hiérarchie des classes dont elle dépend. Peu importe ce qui s'étend - Si vous changez la hiérarchie de Expense vs Person, vous n'avez pas à modifier Employee.

1
Petr Pudlák

dans le premier cas, un sous-trait ou une sous-classe de B peut être mélangé à n'importe quel usage de A. Ainsi, B peut être un trait abstrait.

0
IttayD