Quelle est la différence entre les auto-types et l'hérédité des traits dans Scala?

9

Lorsque googlé, de nombreuses réponses à ce sujet apparaissent. Cependant, je ne pense pas que l'un d'entre eux illustre bien la différence entre ces deux fonctionnalités. Je voudrais donc essayer une fois de plus, en particulier ...

Que peut-on faire avec les auto-types et non avec l'héritage, et vice-versa?

Pour moi, il devrait y avoir une différence physique quantifiable entre les deux, sinon ils sont juste nominalement différents.

Si le caractère A prolonge B ou se self-types B, n'illustrent-ils pas tous les deux qu'être de B est une exigence? Quelle est la différence?

Mark Canlas
la source
Je me méfie des conditions que vous avez fixées pour la prime. D'une part, définissez la différence "physique", étant donné qu'il s'agit uniquement de logiciels. Au-delà de cela, pour tout objet composite que vous créez avec des mixins, vous pouvez probablement créer quelque chose d'approximatif en fonction avec héritage - si vous définissez la fonction uniquement en termes de méthodes visibles. Là où ils différeront, c'est en termes d'extensibilité, de flexibilité et de composabilité.
itsbruce
Si vous avez un assortiment de plaques d'acier de différentes tailles, vous pouvez les boulonner ensemble pour former une boîte ou les souder. D'un point de vue étroit, ces fonctionnalités seraient équivalentes - si vous ignorez le fait que l'un peut facilement être reconfiguré ou étendu et que l'autre ne le peut pas. J'ai le sentiment que vous allez faire valoir qu'ils sont équivalents, même si je serais heureux d'avoir tort si vous en disiez plus sur vos critères.
itsbruce
Je suis plus que familier avec ce que vous dites en général, mais je ne comprends toujours pas quelle est la différence dans ce cas particulier. Pourriez-vous fournir des exemples de code qui montrent qu'une méthode est plus extensible et flexible que l'autre? * Code de base avec extension * Code de base avec auto-types * Fonctionnalité ajoutée au style d'extension * Fonctionnalité ajoutée au style de type automatique
Mark Canlas
OK, je pense que je peux essayer ça avant la fin de la prime;)
itsbruce

Réponses:

11

Si le trait A prolonge B, alors le mélange dans A vous donne précisément B plus tout ce que A ajoute ou étend. En revanche, si trait A a une référence auto qui est explicitement typé B, puis la classe mère ultime doit également mélanger B ou un type descendant de B (et mélanger en premier , ce qui est important).

C'est la différence la plus importante. Dans le premier cas, le type précis de B est cristallisé au point A où il se prolonge. Dans le second, le concepteur de la classe parent doit décider quelle version de B est utilisée, au moment où la classe parent est composée.

Une autre différence est où A et B fournissent des méthodes du même nom. Lorsque A étend B, la méthode de A remplace B. Lorsque A est mélangé après B, la méthode de A gagne simplement.

L'auto référence saisie vous donne beaucoup plus de liberté; le couplage entre A et B est lâche.

MISE À JOUR:

Puisque vous n'êtes pas clair sur les avantages de ces différences ...

Si vous utilisez l'héritage direct, vous créez le trait A qui est B + A. Vous avez fixé la relation dans la pierre.

Si vous utilisez une auto-référence typée, toute personne qui souhaite utiliser votre trait A dans la classe C pourrait

  • Mélanger B puis A dans C.
  • Mélanger un sous-type de B puis A dans C.
  • Mélanger A dans C, où C est une sous-classe de B.

Et ce n'est pas la limite de leurs options, étant donné la façon dont Scala vous permet d'instancier un trait directement avec un bloc de code comme constructeur.

Quant à la différence entre la méthode gagnante de A , parce que A est mélangé en dernier, par rapport à A qui étend B, considérez ceci ...

Lorsque vous mélangez dans une séquence de traits, chaque fois que la méthode foo()est invoquée, le compilateur va au dernier trait mélangé à rechercher foo(), puis (s'il n'est pas trouvé), il parcourt la séquence vers la gauche jusqu'à ce qu'il trouve un trait qui implémente foo()et utilise cette. A a également la possibilité d'appeler super.foo(), qui parcourt également la séquence vers la gauche jusqu'à ce qu'il trouve une implémentation, etc.

Donc, si A a une auto-référence typée à B et que l'auteur de A sait que B implémente foo(), A peut appeler super.foo()sachant que si rien d'autre ne le prévoit foo(), B le fera. Cependant, le créateur de la classe C a la possibilité de supprimer tout autre trait dans lequel il est implémenté foo(), et A l'obtiendra à la place.

Encore une fois, c'est beaucoup plus puissant et moins limitatif que A étendant B et appelant directement la version de B de foo().

itsbruce
la source
Quelle est la différence fonctionnelle entre une victoire et une priorité? Je reçois A dans les deux cas via des mécanismes différents? Et dans votre premier exemple ... Dans votre premier paragraphe, pourquoi ne pas avoir le trait A étendre SuperOfB? Il semble que nous pourrions toujours remodeler le problème en utilisant l'un ou l'autre mécanisme. Je suppose que je ne vois pas de cas d'utilisation où ce n'est pas possible. Ou je suppose trop de choses.
Mark Canlas
Pourquoi voudriez-vous que A étende une sous-classe de B, si B définit ce dont vous avez besoin? L'auto-référence oblige B (ou une sous-classe) à être présent, mais donne le choix au développeur? Ils peuvent mélanger quelque chose qu'ils ont écrit après que vous ayez écrit le trait A, tant qu'il s'étend sur B. Pourquoi les limiter uniquement à ce qui était disponible lorsque vous avez écrit le trait A?
itsbruce
Mis à jour pour rendre la différence super claire.
itsbruce
@itsbruce y a-t-il une différence conceptuelle? IS-A contre HAS-A?
Jas
@Jas Dans le contexte de la relation entre les traits A et B , l'héritage est IS-A tandis que l'auto-référence typée donne HAS-A (une relation de composition). Pour la classe dans laquelle les traits sont mélangés, le résultat est IS-A , peu importe.
itsbruce
0

J'ai du code qui illustre certaines des différences par rapport à la visibilité et aux implémentations "par défaut" lors de l'extension par rapport à la définition d'un auto-type. Il n'illustre aucune des parties déjà discutées sur la façon dont les collisions de noms réelles sont résolues, mais se concentre plutôt sur ce qui est possible et impossible de le faire.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Une différence importante de l'OMI est qu'elle A1n'expose pas qu'elle est nécessaire Bà tout ce qui la voit simplement A1(comme illustré dans les parties montées). Le seul code qui verra réellement qu'une spécialisation particulière Best utilisée est un code qui connaît explicitement le type composé (comme Think*{X,Y}).

Un autre point est que A2(avec extension) utilisera réellement Bsi rien d'autre n'est spécifié tandis que A1(self-type) ne dit pas qu'il utilisera à Bmoins qu'il ne soit surchargé, un B concret doit être explicitement donné lorsque les objets sont instanciés.

Rouzbeh Delavari
la source