web-dev-qa-db-fra.com

Ordre de linéarisation en Scala

J'ai des difficultés à comprendre l'ordre de linéarisation dans Scala lorsque je travaille avec des traits:

class A {
  def foo() = "A"
}

trait B extends A {
  override def foo() = "B" + super.foo()
}

trait C extends B {
  override def foo() = "C" + super.foo()
}

trait D extends A {
  override def foo() = "D" + super.foo()
}

object LinearizationPlayground {
    def main(args: Array[String]) {

      var d = new A with D with C with B;
      println(d.foo) // CBDA????
  }    
}

Il affiche CBDA mais je n'arrive pas à comprendre pourquoi. Comment l'ordre des traits est-il déterminé?

THX

42
woodtluk

Une manière intuitive de raisonner sur la linéarisation est de se référer à l'ordre de construction et de visualiser la hiérarchie linéaire.

Vous pourriez penser de cette façon. La classe de base est construite en premier; mais avant de pouvoir construire la classe de base, ses superclasses/traits doivent être construits en premier (cela signifie que la construction commence au sommet de la hiérarchie). Pour chaque classe de la hiérarchie, les traits mixtes sont construits de gauche à droite car un trait de droite est ajouté "plus tard" et a donc la possibilité de "remplacer" les traits précédents. Cependant, comme pour les classes, pour construire un trait, ses traits de base doivent être construits en premier (évident); et, tout à fait raisonnablement, si un trait a déjà été construit (n'importe où dans la hiérarchie), il n'est pas reconstruit à nouveau. Or, l'ordre de construction est l'inverse de la linéarisation. Considérez les traits/classes "de base" comme plus élevés dans la hiérarchie linéaire, et les traits plus bas dans la hiérarchie comme plus proches de la classe/objet qui fait l'objet de la linéarisation. La linéarisation affecte la façon dont le "super" est résolu dans un trait: il se résoudra au trait de base le plus proche (plus haut dans la hiérarchie).

Donc:

var d = new A with D with C with B;

La linéarisation de A with D with C with B Est

  • (haut de la hiérarchie) A (construit d'abord comme classe de base)
  • linéarisation de D
    • A (not considered as A occurs before)
    • D (D extends A)
  • linéarisation de C
    • A (not considered as A occurs before)
    • B (B extends A)
    • C (C extends B)
  • linéarisation de B
    • A (not considered as A occurs before)
    • B (not considered as B occurs before)

La linéarisation est donc: A-D-B-C. Vous pourriez la considérer comme une hiérarchie linéaire où A est la racine (la plus élevée) et est construite en premier, et C est la feuille (la plus basse) et construite en dernier. Comme C est construit en dernier, cela signifie que cela peut remplacer les membres "précédents".

Compte tenu de ces règles intuitives, d.foo Appelle C.foo, Qui renvoie un "C" suivi de super.foo() qui est résolu sur B (le trait de gauche de B, c'est-à-dire supérieur/avant, dans la linéarisation), qui retourne un "B" suivi de super.foo() qui est résolu sur D, qui retourne un "D" suivi de super.foo() qui est résolu sur A, qui renvoie finalement "A". Vous avez donc "CBDA".

Comme autre exemple, j'ai préparé le suivant:

class X { print("X") }
class A extends X { print("A") }
trait H { print("H") }
trait S extends H { print("S") }
trait R { print("R") }
trait T extends R with H { print("T") }
class B extends A with T with S { print("B") }

new B  // X A R H T S B     (the prints follow the construction order)

// Linearization is the reverse of the construction order.
// Note: the rightmost "H" wins (traits are not re-constructed)
// lin(B) = B >> lin(S) >> lin(T) >> lin(A)
//        = B >> (S >> H) >> (T >> H >> R) >> (A >> X)
//        = B >> S >> T >> H >> R >> A >> X
48
metaphori

La réponse acceptée est merveilleuse, cependant, par souci de simplification, je voudrais faire de mon mieux pour la décrire d'une manière différente. L'espoir peut aider certaines personnes.

Lorsque vous rencontrez un problème de linéarisation, la première étape consiste à dessiner l'arborescence hiérarchique des classes et des traits. Pour cet exemple spécifique, l'arborescence de la hiérarchie ressemblerait à ceci:

enter image description here

La deuxième étape consiste à noter toute la linéarisation des traits et des classes qui interfère avec le problème cible. Vous en aurez besoin tous dans celui qui précède la dernière étape. Pour cela, vous devez simplement écrire le chemin pour atteindre la racine. La linéarisation des traits est la suivante:

L(A) = A
L(C) = C -> B -> A
L(B) = B -> A
L(D) = D -> A

La troisième étape consiste à écrire la linéarisation du problème. Dans ce problème spécifique, nous prévoyons de résoudre la linéarisation de

var d = new A with D with C with B;

Il est important de noter qu'il existe une règle par laquelle il résout l'invocation de méthode en utilisant d'abord la recherche droite d'abord, profondeur d'abord. Dans un autre mot, vous devriez commencer à écrire la linéarisation du côté le plus à droite. C'est comme suit: L (B) >> L (C) >> L (D) >> L (A)

Quatrième étape est l'étape la plus simple. Remplacez simplement chaque linéarisation de la deuxième étape à la troisième étape. Après la substitution, vous aurez quelque chose comme ceci:

B -> A -> C -> B -> A -> D -> A -> A

Last but not least, vous devez maintenant supprimer toutes les classes dupliquées de gauche à droite. Les caractères gras doivent être supprimés: [~ # ~] b [~ # ~] -> [~ # ~] a [~ # ~] -> C -> B -> [~ # ~] a [~ # ~] -> D -> [~ # ~] a [~ # ~] -> A

Vous voyez, vous avez le résultat: [~ # ~] c [~ # ~] -> [~ # ~] b [~ # ~] -> [~ # ~] d [~ # ~] - > [~ # ~] a [~ # ~] Par conséquent, la réponse est CBDA.

Je sais que ce n'est pas une description conceptuelle individuellement approfondie, mais cela peut aider en complément de la description conceptuelle, je suppose.


Et cette partie explique en s'appuyant sur la formule:

 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), Lin(D)}
 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), {D, Lin(A)}}
 Lin(new A with D with C with B) = {A, Lin(B), Lin(C), {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, Lin(B)}, {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, {B, Lin(A)}}, {D, A}}
 Lin(new A with D with C with B) = {A, Lin(B), {C, {B, A}}, {D, A}}
 Lin(new A with D with C with B) = {A, {B, A}, {C, {B, A}}, {D, A}}
 Lin(new A with D with C with B) = {C,B,D,A}
15
Mehran

La pile de traits de Scala, vous pouvez donc les regarder en les ajoutant un à la fois:

  1. Commencer avec new A => foo = "A"
  2. Pile with D => foo = "DA"
  3. Pile with C qui empile with B => foo = "CBDA"
  4. Pile with B ne fait rien car B est déjà empilé dans C => foo = "CBDA"

Voici un article de blog sur la façon dont Scala résout le problème d'héritage des diamants.

12
Noah

Le processus par lequel scala résoudre le super appel est appelé Linéarisation Dans votre exemple, vous créez un objet comme

var d = new A with D with C with B;

Donc, comme spécifié scala documents de référence ici l'appel à super sera résolu comme

l(A) = A >> l(B) >> l(c) >> l(D)

l(A) = A >> B >> l(A) >> l(C) >> l(D)

l(A) = A >> B >> A >> C >> l(B) >> l(D)

l(A) = A >> B >> A >> C >> B >> l(A) >> l(D)

l(A) = A >> B >> A >> C >> B >> A >> l(D)

l(A) = A >> B >> A >> C >> B >> A >> D >> l(A)

l(A) = A >> B >> A >> C >> B >> A >> D >> A

Maintenant, commencez par la gauche et supprimez la construction en double dans laquelle la droite en gagnera une

par exemple. retirer A et nous obtenons

l(A) = B >> C >> B >> D >> A

retirer B et nous obtenons

l(A) = C >> B >> D >> A

Ici, nous n'avons aucune entrée en double Commençons maintenant à appeler depuis C

C B D A

super.foo dans la classe C appellera foo dans B et foo dans B appellera foo dans D et ainsi de suite.

P.S. ici l(A) est la linéarisation de A

6
optional

En plus des autres réponses, vous pouvez trouver une explication étape par étape dans le résultat de l'extrait ci-dessous

hljs.initHighlightingOnLoad();
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.0.0/highlight.min.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.0.0/styles/zenburn.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
<table class="table">
  <tr>
    <th>Expression</th>
    <th>type</th>
    <th><code>foo()</code> result</th>
  </tr>

  <tr>
    <td><pre><code class="scala"> new A </code></pre>
    </td>
    <td><pre><code class="scala"> A </code></pre>
    </td>
    <td><pre><code class="scala">"A"</code></pre>
    </td>
  </tr>

  <tr>
    <td><pre><code class="scala"> new A with D </code></pre>
    </td>
    <td><pre><code class="scala"> D </code></pre>
    </td>
    <td><pre><code class="scala">"DA"</code></pre>
    </td>
  </tr>
  
    <tr>
    <td><pre><code class="scala"> new A with D with C </code></pre>
    </td>
    <td><pre><code class="scala"> D with C </code></pre>
    </td>
    <td><pre><code class="scala">"CBDA"</code></pre>
    </td>
  </tr>
  
   <tr>
    <td><pre><code class="scala"> new A with D with C with B </code></pre>
    </td>
    <td><pre><code class="scala"> D with C </code></pre>
    </td>
    <td><pre><code class="scala">"CBDA"</code></pre>
    </td>
  </tr>
</table>
1
Odomontois

explication, comment le compilateur voit une classe Combined qui étend les traits A with D with C with B

class Combined extends A with D with C with B {
  final <superaccessor> <artifact> def super$foo(): String = B$class.foo(Combined.this);
  override def foo(): String = C$class.foo(Combined.this);
  final <superaccessor> <artifact> def super$foo(): String = D$class.foo(Combined.this);
  final <superaccessor> <artifact> def super$foo(): String =  Combined.super.foo();
  def <init>(): Combined = {
    Combined.super.<init>();
    D$class./*D$class*/$init$(Combined.this);
    B$class./*B$class*/$init$(Combined.this);
    C$class./*C$class*/$init$(Combined.this);
    ()
  }
};

exemple réduit

Vous pouvez lire de gauche à droite. Voici un petit exemple. Les trois traits imprimeront leur nom une fois initialisés, c'est-à-dire étendus:

scala> trait A {println("A")}
scala> trait B {println("B")}
scala> trait C {println("C")}

scala> new A with B with C
  A
  B
  C
res0: A with B with C = $anon$1@5e025e70

scala> new A with C with B
 A
 C
 B
res1: A with C with B = $anon$1@2ed94a8b

Il s'agit donc de l'ordre de linéarisation de base. Ainsi, le dernier remplacera le précédent.

Votre problème est un peu plus complexe. Comme vous les traits étendez déjà d'autres traits qui remplacent eux-mêmes certaines valeurs des traits précédents. Mais l'ordre d'initialisation left to right ou right will override left.

Vous devez garder à l'esprit que le trait lui-même sera initialisé en premier.

1
Andreas Neumann

Eh bien, en fait, je vois que vous venez d'inverser la linéarisation du constructeur, ce qui, je pense, est assez simple, alors commençons par comprendre la linéarisation du constructeur

Premier exemple

object Linearization3 {
  def main(args: Array[String]) {
    var x = new X
    println()
    println(x.foo)
  }
}

class A {
  print("A")

  def foo() = "A"
}

trait B extends A {
  print("B")

  override def foo() =   super.foo() + "B" // Hence I flipped yours to give exact output as constructor
}

trait C extends B {
  print("C")

  override def foo() =  super.foo() + "C"
}

trait D extends A {
  print("D")

  override def foo() =  super.foo() + "D"
}

class X extends A with D with C with B

Quelles sorties:

ADBC
ADBC

Donc, pour calculer la sortie, je prends juste la classe/les traits un par un de gauche à droite, puis j'écris récursivement les sorties (sans doublons) voici comment:

  1. Notre signature de classe est: class X extends A with D with C with B
  2. Donc le premier est A, puisque A n'a pas de parents (deadend) il suffit d'imprimer son constructeur
  3. Maintenant D, qui étend A, puisque nous avons déjà imprimé A, imprimons D
  4. Maintenant C, qui étend B, qui étend A, donc nous sautons A parce qu'il a déjà été imprimé, nous imprimons ensuite B, puis imprimons C (c'est comme une fonction récursive)
  5. Maintenant B, qui étend A, nous sautons A, et nous sautons également B (rien imprimé)
  6. et vous avez obtenu ADBC!

Exemple inversé (votre exemple)

object Linearization3 {
  def main(args: Array[String]) {
    var x = new X
    println()
    println(x.foo)
  }
}

class A {
  print("A")

  def foo() = "A"
}

trait B extends A {
  print("B")

  override def foo() = "B" + super.foo()
}

trait C extends B {
  print("C")

  override def foo() = "C" + super.foo()
}

trait D extends A {
  print("D")

  override def foo() = "D" + super.foo()
}

class X extends A with D with C with B

La sortie est:

ADBC
CBDA

J'espère que c'était assez simple pour des débutants comme moi

0
Ismail Marmoush