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

387

Un auto-type pour un trait A:

trait B
trait A { this: B => }

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

En revanche, les éléments suivants:

trait B
trait A extends B

dit que "toute classe (concrète ou abstraite) se mélangeant Asera également mélangée en B" .

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

Qu'est-ce que je rate?

Dave
la source
Je m'intéresse en fait ici aux différences entre les types de soi et le sous-classement des traits. Je connais certaines des utilisations courantes des auto-types; Je ne peux tout simplement pas trouver une raison pour laquelle ils ne seraient pas plus clairement effectués de la même manière avec le sous-typage.
Dave
32
On peut utiliser des paramètres de type dans les auto-types: trait A[Self] {this: Self => }c'est légal, trait A[Self] extends Selfnon.
Blaisorblade
3
Un auto-type peut également être une classe, mais un trait ne peut pas hériter d'une classe.
cvogt
10
@cvogt: un trait peut hériter d'une classe (au moins à partir de 2.10): pastebin.com/zShvr8LX
Erik Kaplun
1
@Blaisorblade: n'est-ce pas quelque chose qui pourrait être résolu par une petite refonte du langage, et non une limitation fondamentale? (au moins du point de vue de la question)
Erik Kaplun

Réponses:

273

Il est principalement utilisé pour l' 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 "Cake Pattern and Scala", vous obtiendrez de nombreux liens, y compris des présentations et des vidéos. Pour l'instant, voici un lien vers une autre question .

Maintenant, quelle est 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 c'est un A. Lorsque vous utilisez des auto-types, B nécessite un A. Il existe deux exigences spécifiques créées avec des auto-types:

  1. Si Best étendu, alors vous devez mélanger dans un A.
  2. Lorsqu'une classe concrète étend / mélange finalement ces traits, une classe / trait doit être implémenté 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
                       ^

S'il Tweeters'agissait d'une sous-classe de User, il n'y aurait pas d'erreur. Dans le code ci-dessus, nous avions besoin d' un à Userchaque Tweeterfois, mais Useraucun n'était fourni Wrong, nous avons donc eu une erreur. Maintenant, avec le code ci-dessus toujours dans la portée, considérez:

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, l'exigence de mélanger un Userest satisfaite. Cependant, la deuxième exigence mentionnée ci-dessus n'est pas satisfaite: la charge de la mise Useren œuvre demeure pour les classes / caractères qui s'étendent Right.

Avec les RightAgaindeux exigences sont satisfaites. A Useret une implémentation de Usersont fournis.

Pour des cas d'utilisation plus pratiques, veuillez consulter les liens au début de cette réponse! Mais j'espère que vous comprenez maintenant.

Daniel C. Sobral
la source
3
Merci. Le motif de gâteau est à 90% de ce que je veux dire, pourquoi je parle du battage médiatique autour des auto-types ... c'est là que j'ai vu le sujet pour la première fois. L'exemple de Jonas Boner est excellent, car il souligne le point de ma question. Si vous avez changé les auto-types dans son exemple de chauffage pour être des sous-portraits, alors quelle serait la différence (à part l'erreur que vous obtenez lors de la définition du ComponentRegistry si vous ne mélangez pas les bonnes choses?
Dave
29
@Dave: Tu veux dire comme trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Cela entraînerait WarmerComponentImpld'avoir ces interfaces. Ils seraient disponibles pour tout ce qui s'étend WarmerComponentImpl, ce qui est clairement faux, car ce n'est pas un SensorDeviceComponent, ni un OnOffDeviceComponent. En tant que type autonome, ces dépendances sont disponibles exclusivement pour WarmerComponentImpl. Un Listpourrait être utilisé comme un Array, et vice versa. Mais ce n'est tout simplement pas la même chose.
Daniel C.Sobral
10
Merci Daniel. C'est probablement la principale distinction que je recherchais. Le problème pratique est que l'utilisation du sous-classement entraînera une fuite de fonctionnalités dans votre interface que vous n'avez pas l'intention. C'est le résultat de la violation de la règle plus théorique «fait partie d'une» pour les traits. Les auto-types expriment une relation "utilise-une" entre les parties.
Dave
11
@Rodney Non, ça ne devrait pas. En fait, utiliser thisavec des types personnels est quelque chose que je méprise, car il masque sans raison l'original this.
Daniel C.Sobral
9
@opensas Essayez self: Dep1 with Dep2 =>.
Daniel C.Sobral
156

Les auto-types vous permettent de définir des dépendances cycliques. Par exemple, vous pouvez y parvenir:

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

L'héritage utilisant extendsne permet pas cela. Essayer:

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

Dans le livre Odersky, consultez la section 33.5 (chapitre Création de l'interface utilisateur de feuille de calcul) où il mentionne:

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

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

J'espère que cela t'aides.

Mushtaq Ahmed
la source
3
Je n'avais pas envisagé ce scénario. C'est le premier exemple de quelque chose que j'ai vu qui n'est pas la même chose qu'un auto-type qu'avec une sous-classe. Cependant, cela semble une sorte de casse de bord et, plus important encore, cela semble être une mauvaise idée (je fais généralement beaucoup de chemin pour NE PAS définir les dépendances cycliques!). Trouvez-vous que c'est la distinction la plus importante?
Dave
4
Je le pense. Je ne vois aucune autre raison pour laquelle je préférerais les auto-types à la clause d'extension. Les auto-types sont verbeux, ils ne sont pas hérités (vous devez donc ajouter des auto-types à tous les sous-types en tant que rituel) et vous ne pouvez voir que les membres mais ne pouvez pas les remplacer. Je connais bien le modèle Cake et de nombreux articles mentionnant des auto-types pour DI. Mais je ne suis pas convaincu. J'avais créé un exemple d'application ici depuis longtemps ( bitbucket.org/mushtaq/scala-di ). Regardez spécifiquement le dossier / src / configs. J'ai atteint DI pour remplacer les configurations de ressorts complexes sans auto-types.
Mushtaq Ahmed
Mushtaq, nous sommes d'accord. Je pense que la déclaration de Daniel à propos de ne pas exposer les fonctionnalités non intentionnelles est importante mais, comme vous le dites, il y a une vue miroir de cette `` fonctionnalité '' ... que vous ne pouvez pas remplacer la fonctionnalité ou l'utiliser dans de futures sous-classes. Cela me dit assez clairement quand le design exigera l'un par rapport à l'autre. J'éviterai les auto-types jusqu'à ce que je trouve un véritable besoin - c'est-à-dire si je commence à utiliser des objets comme modules comme le souligne Daniel. Je fais du câblage automatique des dépendances avec des paramètres implicites et un objet bootstrapper simple. J'aime la simplicité.
Dave
@ DanielC.Sobral peut être grâce à votre commentaire mais pour le moment il a plus de votes positifs que votre anser. Upvoting both :)
rintcius
Pourquoi ne pas simplement créer un trait AB? Comme les traits A et B doivent toujours être combinés dans n'importe quelle classe finale, pourquoi les séparer en premier lieu?
Rich Oliver
56

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

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

Le type auto ici est un type structurel. L'effet est de dire que tout ce qui se mélange dans Foo doit implémenter une unité de retour de méthode sans argument "close". Cela permet des mixins sûrs pour la frappe de canard.

Dave Griffith
la source
41
En fait, vous pouvez également utiliser l'héritage avec des types structurels: la classe abstraite A étend {def close: Unit}
Adrian
12
Je pense que le typage structurel utilise la réflexion, alors utilisez-le uniquement quand il n'y a pas d'autre choix ...
Eran Medan
@Adrian, je pense que votre commentaire est incorrect. `La classe abstraite A étend {def close: Unit}` est juste une classe abstraite avec une superclasse Object. c'est juste une syntaxe permissive de Scala aux expressions absurdes. Vous pouvez `la classe X étend {def f = 1}; nouveau X (). f` par exemple
Alexey
1
@Alexey Je ne vois pas pourquoi votre exemple (ou le mien) est insensé.
Adrian
1
@Adrian, abstract class A extends {def close:Unit}est équivalent à abstract class A {def close:Unit}. Il ne s'agit donc pas de types structurels.
Alexey
13

La section 2.3 "Annotations Selftype" de l'article original Scala de Martin Odersky, Scalable Component Abstractions, explique en fait très bien le but du Selftype au-delà de la composition mixin: fournir une autre manière d'associer une classe à un type abstrait.

L'exemple donné dans l'article était le suivant, et il ne semble pas avoir d'élégant correspondant de 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;
  }
}
lcn
la source
Pour ceux qui se demandent pourquoi le sous-classement ne résoudra pas cela, la section 2.3 dit également ceci: «Chacun des opérandes d'une composition de mixage C_0 avec ... avec C_n, doit faire référence à une classe. Le mécanisme de composition mixin ne permet à aucun C_i de se référer à un type abstrait. Cette restriction permet de vérifier statiquement les ambiguïtés et de contourner les conflits au moment où une classe est composée. »
Luke Maurer
12

Une autre chose qui n'a pas été mentionnée: comme les auto-types ne font pas partie de la hiérarchie de la classe requise, ils peuvent être exclus de la correspondance de modèles, 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
Bruno Bieth
la source
10

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

  • Les types que vous étendez sont exposés à des types hérités, mais les auto-types ne le sont pas

    par exemple: class Cow { this: FourStomachs }vous permet d'utiliser des méthodes disponibles uniquement pour les ruminants, telles que digestGrass. Les traits qui prolongent la vache n'auront cependant pas de tels privilèges. D'un autre côté, class Cow extends FourStomachsexposera digestGrassà quiconque extends Cow .

  • les auto-types autorisent des dépendances cycliques, l'extension d'autres types ne

jazmit
la source
9

Commençons par la dépendance cyclique.

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

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

Cependant, la modularité de cette solution n'est pas aussi grande qu'elle pourrait le paraître à première vue, car vous pouvez remplacer les auto-types comme suit:

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

Bien que, si vous remplacez un membre d'un auto-type, vous perdez l'accès au membre d'origine, qui peut toujours être consulté via super héritage. 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 principale méthode d'application de la modularité est la composition plutôt que l'héritage ou les types 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 self est que je trouve beaucoup plus difficile d'obtenir l'ordre d'initialisation correct avec les types self. Cependant, il y a certaines choses que vous pouvez faire avec les types d'individu que vous ne pouvez pas faire avec l'héritage. Les auto-types 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 je pense certainement qu'il serait utile d'avoir des classes et des traits de constructeur de chemin comme nous avons des traits / classes de constructeur de type. 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 davantage souligné est que les traits peuvent étendre les classes. Merci à David Maclver de 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]

ScnBasehérite de la classe Swing Frame, il peut donc être utilisé comme un auto-type puis mélangé à la fin (à l'instanciation). Cependant, val geomRdoit être initialisé avant d'être utilisé par l'héritage de traits. Nous avons donc besoin d'une classe pour appliquer l'initialisation préalable de geomR. La classe ScnVistapeut alors être héritée de plusieurs traits orthogonaux qui peuvent eux-mêmes être hérités. L'utilisation de plusieurs paramètres de type (génériques) offre une autre forme de modularité.

Rich Oliver
la source
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
la source
4

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

kikibobo
la source
3
@Blaisorblade: Je me demande si vous avez peut-être mal lu la réponse de kikibobo - le type de soi d'un trait vous permet en effet de contraindre les types qui peuvent le mélanger, et cela fait partie de son utilité. Par exemple, si nous définissons trait A { self:B => ... }une déclaration X with An'est valide que si X étend B. Oui, vous pouvez dire X with A with Q, où Q ne s'étend pas B, mais je crois que le point de kikibobo était que X est si contraint. Ou ai-je raté quelque chose?
AmigoNico
1
Merci, vous avez raison. Mon vote a été verrouillé, mais heureusement, j'ai pu modifier la réponse, puis modifier mon vote.
Blaisorblade
1

Mise à jour: Une différence principale est que les auto-types peuvent dépendre de plusieurs classes (j'admets que c'est un peu le cas du coin). 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 Employeemixage à tout ce qui est une sous-classe de Personet Expense. Bien sûr, cela n'a de sens que s'il Expenses'étend Personou vice versa. Le fait est que l'utilisation de self-types Employeepeut être indépendante de la hiérarchie des classes dont elle dépend. Il ne se soucie pas de ce qui étend quoi - Si vous changez la hiérarchie de Expensevs Person, vous n'avez pas à modifier Employee.

Petr Pudlák
la source
L'employé n'a pas besoin d'être une classe pour descendre de la personne. Les traits peuvent prolonger les classes. Si le trait Employé étendait Personne au lieu d'utiliser un auto-type, l'exemple fonctionnerait toujours. Je trouve votre exemple intéressant, mais il ne semble pas illustrer un cas d'utilisation pour les auto-types.
Morgan Creighton
@MorganCreighton Assez juste, je ne savais pas que les traits pouvaient étendre les classes. J'y penserai si je peux trouver un meilleur exemple.
Petr Pudlák
1
Oui, c'est une fonctionnalité linguistique surprenante. Si le trait Employé étend la classe Personne, alors quelle que soit la classe finalement "perdue", l'employé devra également étendre Personne. Mais cette restriction est toujours présente si l'employé a utilisé un auto-type au lieu d'étendre Personne. Santé, Petr!
Morgan Creighton
1
Je ne vois pas pourquoi "cela n'a de sens que si Expense étend Personne ou vice versa."
Robin Green
0

dans le premier cas, un sous-trait ou une sous-classe de B peut être mélangé à toutes les utilisations A. Donc B peut être un trait abstrait.

IttayD
la source
Non, B peut être (et est en fait) un "trait abstrait" dans les deux cas. Il n'y a donc pas de différence de ce point de vue.
Robin Green