Was ist der Unterschied zwischen Selbsttypen und Unterklassen von Merkmalen?

387

Ein Selbsttyp für ein Merkmal A:

trait B
trait A { this: B => }

sagt, dass " Anicht in eine konkrete Klasse gemischt werden kann, die sich nicht auch erstreckt B" .

Auf der anderen Seite die folgenden:

trait B
trait A extends B

sagt, dass "jede (konkrete oder abstrakte) Klasse, die sich einmischt A, auch in B einmischt" .

Bedeuten diese beiden Aussagen nicht dasselbe? Der Selbsttyp scheint nur dazu zu dienen, die Möglichkeit eines einfachen Fehlers bei der Kompilierung zu schaffen.

Was vermisse ich?

Dave
quelle
Ich interessiere mich hier tatsächlich für die Unterschiede zwischen Selbsttypen und Unterklassen in Merkmalen. Ich kenne einige der gebräuchlichen Verwendungszwecke für Selbsttypen. Ich kann einfach keinen Grund finden, warum sie mit der Subtypisierung nicht klarer gemacht würden.
Dave
32
Man kann Typparameter innerhalb von Selbsttypen verwenden: trait A[Self] {this: Self => }ist legal, trait A[Self] extends Selfnicht.
Blaisorblade
3
Ein Selbsttyp kann auch eine Klasse sein, aber ein Merkmal kann nicht von einer Klasse erben.
cvogt
10
@cvogt: Ein Merkmal kann von einer Klasse erben (mindestens ab 2.10): pastebin.com/zShvr8LX
Erik Kaplun
1
@Blaisorblade: Ist das nicht etwas, das durch eine kleine Neugestaltung der Sprache gelöst werden könnte, und keine grundlegende Einschränkung? (zumindest aus Sicht der Frage)
Erik Kaplun

Antworten:

273

Es wird hauptsächlich für die Abhängigkeitsinjektion verwendet , z. B. im Kuchenmuster. Es gibt einen großartigen Artikel über viele verschiedene Formen der Abhängigkeitsinjektion in Scala, einschließlich des Kuchenmusters. Wenn Sie "Kuchenmuster und Scala" von Google verwenden, erhalten Sie viele Links, einschließlich Präsentationen und Videos. Im Moment ist hier ein Link zu einer anderen Frage .

Was nun der Unterschied zwischen einem Selbsttyp und der Erweiterung eines Merkmals ist, ist einfach. Wenn Sie sagen B extends A, dann B ist ein A. Wenn Sie sich selbst Typen verwenden, B erfordert ein A. Es gibt zwei spezifische Anforderungen, die mit Selbsttypen erstellt werden:

  1. Wenn Bverlängert wird, dann sind Sie erforderlich , um Verwechslungen in ein A.
  2. Wenn eine konkrete Klasse diese Eigenschaften schließlich erweitert / einmischt, muss eine Klasse / ein Merkmal implementiert werden A.

Betrachten Sie die folgenden Beispiele:

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
                       ^

Wenn Tweetereine Unterklasse von wäre User, würde es keinen Fehler geben. In dem obigen Code, wir erforderlich ein , Userwann immer Tweeterverwendet wird, jedoch ein Usernicht zur Verfügung gestellt wurde Wrong, so dass wir einen Fehler bekamen. Berücksichtigen Sie nun, da der obige Code noch im Gültigkeitsbereich ist:

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

Damit Rightist die Anforderung zum Einmischen von a Usererfüllt. Die oben genannte zweite Anforderung ist jedoch nicht erfüllt: Die Umsetzungslast Userbleibt weiterhin für Klassen / Merkmale, die sich erstrecken Right.

Mit RightAgainbeiden Anforderungen sind erfüllt. A Userund eine Implementierung von Userwerden bereitgestellt.

Weitere praktische Anwendungsfälle finden Sie unter den Links am Anfang dieser Antwort! Aber hoffentlich verstehst du es jetzt.

Daniel C. Sobral
quelle
3
Vielen Dank. Das Kuchenmuster ist zu 90% das, was ich meine, warum ich über den Hype um Selbsttypen spreche ... hier habe ich das Thema zum ersten Mal gesehen. Jonas Boners Beispiel ist großartig, weil es den Punkt meiner Frage unterstreicht. Wenn Sie die Selbsttypen in seinem Heizungsbeispiel in Subtraits geändert hätten, was wäre dann der Unterschied (abgesehen von dem Fehler, den Sie beim Definieren der ComponentRegistry erhalten, wenn Sie nicht das richtige Material einmischen?
Dave
29
@ Dave: Du meinst wie trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Das würde dazu führen WarmerComponentImpl, dass diese Schnittstellen vorhanden sind. Sie wären für alles verfügbar, was erweitert wurde WarmerComponentImpl, was eindeutig falsch ist, da es weder ein SensorDeviceComponentnoch ein ist OnOffDeviceComponent. Als Selbsttyp stehen diese Abhängigkeiten ausschließlich zur Verfügung WarmerComponentImpl. A Listkönnte als Arrayund umgekehrt verwendet werden. Aber sie sind einfach nicht dasselbe.
Daniel C. Sobral
10
Danke Daniel. Dies ist wahrscheinlich der Hauptunterschied, den ich gesucht habe. Das praktische Problem besteht darin, dass durch die Verwendung von Unterklassen Funktionen in Ihre Schnittstelle gelangen, die Sie nicht beabsichtigen. Es ist ein Ergebnis der Verletzung der theoretischeren "Ist-Teil-einer" -Regel für Eigenschaften. Selbsttypen drücken eine "Verwendungsbeziehung" zwischen Teilen aus.
Dave
11
@ Rodney Nein, sollte es nicht. In der Tat ist die Verwendung thismit Selbsttypen etwas, auf das ich herabschaue, da es das Original ohne guten Grund beschattet this.
Daniel C. Sobral
9
@opensas Versuchen self: Dep1 with Dep2 =>.
Daniel C. Sobral
156

Mit Selbsttypen können Sie zyklische Abhängigkeiten definieren. Zum Beispiel können Sie dies erreichen:

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

Vererbung mit extendserlaubt das nicht. Versuchen:

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

Lesen Sie im Odersky-Buch Abschnitt 33.5 (Kapitel zum Erstellen einer Tabellenkalkulations-Benutzeroberfläche), in dem Folgendes erwähnt wird:

Im Tabellenkalkulationsbeispiel erbt die Klasse Model von Evaluator und erhält somit Zugriff auf seine Evaluierungsmethode. Um den anderen Weg zu gehen, definiert der Klassenevaluator seinen Selbsttyp wie folgt als Modell:

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

Hoffe das hilft.

Mushtaq Ahmed
quelle
3
Ich hatte dieses Szenario nicht in Betracht gezogen. Es ist das erste Beispiel für etwas, das ich gesehen habe und das nicht mit einem Selbsttyp identisch ist wie mit einer Unterklasse. Es scheint jedoch eine Art Randfall zu sein, und, was noch wichtiger ist, es scheint eine schlechte Idee zu sein (ich bin normalerweise sehr bemüht, keine zyklischen Abhängigkeiten zu definieren!). Finden Sie dies die wichtigste Unterscheidung?
Dave
4
Ich glaube schon. Ich sehe keinen anderen Grund, warum ich Selbsttypen der erweiterten Klausel vorziehen würde. Selbsttypen sind ausführlich, sie werden nicht vererbt (daher müssen Sie allen Untertypen als Ritual Selbsttypen hinzufügen) und Sie können nur Mitglieder sehen, diese aber nicht überschreiben. Ich kenne das Kuchenmuster und viele Beiträge, in denen Selbsttypen für DI erwähnt werden. Aber irgendwie bin ich nicht überzeugt. Ich hatte hier vor langer Zeit eine Beispiel-App erstellt ( bitbucket.org/mushtaq/scala-di ). Schauen Sie sich speziell den Ordner / src / configs an. Ich habe DI erreicht, um komplexe Federkonfigurationen ohne Selbsttypen zu ersetzen.
Mushtaq Ahmed
Mushtaq, wir sind uns einig. Ich denke, Daniels Aussage, keine unbeabsichtigte Funktionalität preiszugeben, ist wichtig, aber wie Sie sagen, gibt es eine Spiegelansicht dieser 'Funktion' ... dass Sie die Funktionalität nicht überschreiben oder in zukünftigen Unterklassen verwenden können. Dies sagt mir ziemlich deutlich, wann Design übereinander verlangt. Ich werde Selbsttypen vermeiden, bis ich ein echtes Bedürfnis finde - dh wenn ich anfange, Objekte als Module zu verwenden, wie Daniel betont. Ich verdrahte automatisch Abhängigkeiten mit impliziten Parametern und einem einfachen Bootstrapper-Objekt. Ich mag die Einfachheit.
Dave
@ DanielC.Sobral mag dank deines Kommentars sein, aber im Moment hat es mehr positive Stimmen als dein Anser. Upvoting beide :)
Rintcius
Warum nicht einfach ein Merkmal AB erstellen? Da die Merkmale A und B in jeder Abschlussklasse immer kombiniert werden müssen, warum sollten sie überhaupt getrennt werden?
Rich Oliver
56

Ein zusätzlicher Unterschied besteht darin, dass Selbsttypen Nichtklassentypen angeben können. Zum Beispiel

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

Der Selbsttyp ist hier ein Strukturtyp. Der Effekt ist zu sagen, dass alles, was in Foo gemischt wird, eine No-Arg-Methode zum Schließen der Methode "close" implementieren muss. Dies ermöglicht sichere Mixins für die Ententypisierung.

Dave Griffith
quelle
41
Tatsächlich können Sie die Vererbung auch für Strukturtypen verwenden: abstrakte Klasse A erweitert {def close: Unit}
Adrian
12
Ich denke, strukturelle Typisierung verwendet Reflexion, also nur verwenden, wenn es keine andere Wahl gibt ...
Eran Medan
@Adrian, ich glaube dein Kommentar ist falsch. `abstrakte Klasse A erweitert {def close: Unit}` ist nur eine abstrakte Klasse mit Objekt-Superklasse. Es ist nur die zulässige Syntax einer Scala für unsinnige Ausdrücke. Sie können `Klasse X erweitert {def f = 1}; neues X (). f` zum Beispiel
Alexey
1
@ Alexander Ich verstehe nicht, warum dein (oder mein) Beispiel unsinnig ist.
Adrian
1
@Adrian, abstract class A extends {def close:Unit}entspricht abstract class A {def close:Unit}. Es handelt sich also nicht um Strukturtypen.
Alexey
13

Abschnitt 2.3 "Selftype Annotations" von Martin Oderskys Original-Scala-Papier Scalable Component Abstractions erklärt den Zweck des Selftype über die Mixin-Komposition hinaus sehr gut: Bieten Sie eine alternative Möglichkeit, eine Klasse einem abstrakten Typ zuzuordnen.

Das Beispiel in der Arbeit war wie folgt und es scheint keinen eleganten Korrespondenten für Unterklassen zu haben:

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
quelle
Für diejenigen, die sich fragen, warum Unterklassen dies nicht lösen, heißt es in Abschnitt 2.3 auch: „Jeder der Operanden einer Mixin-Komposition C_0 mit ... mit C_n muss sich auf eine Klasse beziehen. Der Mixin-Zusammensetzungsmechanismus erlaubt keinem C_i, sich auf einen abstrakten Typ zu beziehen. Diese Einschränkung ermöglicht es, statisch auf Mehrdeutigkeiten zu prüfen und Konflikte an dem Punkt zu überschreiben, an dem eine Klasse zusammengesetzt ist. “
Luke Maurer
12

Eine andere Sache, die nicht erwähnt wurde: Da Selbsttypen nicht Teil der Hierarchie der erforderlichen Klasse sind, können sie vom Musterabgleich ausgeschlossen werden, insbesondere wenn Sie einen umfassenden Abgleich mit einer versiegelten Hierarchie durchführen. Dies ist praktisch, wenn Sie orthogonale Verhaltensweisen modellieren möchten, z.

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
quelle
10

TL; DR Zusammenfassung der anderen Antworten:

  • Von Ihnen erweiterte Typen sind geerbten Typen ausgesetzt, Selbsttypen jedoch nicht

    Beispiel: class Cow { this: FourStomachs }Ermöglicht die Verwendung von Methoden, die nur Wiederkäuern zur Verfügung stehen, z digestGrass. Eigenschaften, die Cow erweitern, haben jedoch keine solchen Privilegien. Auf der anderen Seite class Cow extends FourStomachswird digestGrassjeder aussetzen , der extends Cow .

  • Selbsttypen ermöglichen zyklische Abhängigkeiten, andere Typen nicht

jazmit
quelle
9

Beginnen wir mit der zyklischen Abhängigkeit.

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

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

Die Modularität dieser Lösung ist jedoch nicht so groß, wie es zunächst erscheinen mag, da Sie Selbsttypen wie folgt überschreiben können:

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

Wenn Sie jedoch ein Mitglied eines Selbsttyps überschreiben, verlieren Sie den Zugriff auf das ursprüngliche Mitglied, auf das über Super-Vererbung weiterhin zugegriffen werden kann. Was also wirklich durch die Verwendung von Vererbung gewonnen wird, ist:

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

Jetzt kann ich nicht behaupten, alle Feinheiten des Kuchenmusters zu verstehen, aber es fällt mir auf, dass die Hauptmethode zur Durchsetzung der Modularität eher in der Komposition als in der Vererbung oder in den Selbsttypen besteht.

Die Vererbungsversion ist kürzer, aber der Hauptgrund, warum ich die Vererbung gegenüber Selbsttypen bevorzuge, ist, dass ich es viel schwieriger finde, die Initialisierungsreihenfolge bei Selbsttypen korrekt zu machen. Es gibt jedoch einige Dinge, die Sie mit Selbsttypen tun können, die Sie mit Vererbung nicht tun können. Selbsttypen können einen Typ verwenden, während für die Vererbung ein Merkmal oder eine Klasse wie folgt erforderlich ist:

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

Sie können sogar tun:

trait TypeBuster
{ this: Int with String => }

Obwohl Sie es nie instanziieren können. Ich sehe keinen absoluten Grund dafür, nicht von einem Typ erben zu können, aber ich halte es auf jeden Fall für nützlich, Pfadkonstruktorklassen und -merkmale zu haben, da wir Typkonstruktormerkmale / -klassen haben. Da leider

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

Wir haben das:

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 }

Oder dieses:

  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 }

Ein Punkt, der mehr verstanden werden sollte, ist, dass Eigenschaften Klassen erweitern können. Vielen Dank an David Maclver für diesen Hinweis. Hier ist ein Beispiel aus meinem eigenen 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]

ScnBaseerbt von der Swing Frame-Klasse, sodass sie als Selbsttyp verwendet und am Ende (bei der Instanziierung) eingemischt werden kann. Allerdings val geomRmuss initialisiert werden , bevor es durch Vererbungseigenschaften verwendet wird . Wir brauchen also eine Klasse, um die vorherige Initialisierung von zu erzwingen geomR. Die Klasse ScnVistakann dann von mehreren orthogonalen Merkmalen geerbt werden, von denen selbst geerbt werden kann. Die Verwendung mehrerer Typparameter (Generika) bietet eine alternative Form der Modularität.

Rich Oliver
quelle
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
quelle
4

Mit einem Selbsttyp können Sie angeben, welche Typen ein Merkmal mischen dürfen. Wenn Sie beispielsweise ein Merkmal mit einem CloseableSelbsttyp haben, weiß dieses Merkmal, dass die einzigen Dinge, die es einmischen dürfen, die CloseableSchnittstelle implementieren müssen .

Kikibobo
quelle
3
@Blaisorblade: Ich frage mich, ob Sie möglicherweise die Antwort von Kikibobo falsch verstanden haben - der Selbsttyp eines Merkmals ermöglicht es Ihnen tatsächlich, die Typen einzuschränken, die es mischen können, und das ist Teil seiner Nützlichkeit. Wenn wir zum Beispiel definieren, trait A { self:B => ... }ist eine Deklaration X with Anur gültig, wenn X B erweitert. Ja, Sie können sagen X with A with Q, wo Q B nicht erweitert, aber ich glaube, Kikibobos Punkt war, dass X so eingeschränkt ist. Oder habe ich etwas verpasst?
AmigoNico
1
Danke, du hast recht. Meine Stimme war gesperrt, aber zum Glück konnte ich die Antwort bearbeiten und dann meine Stimme ändern.
Blaisorblade
1

Update: Ein Hauptunterschied besteht darin, dass Selbsttypen von mehreren Klassen abhängen können (ich gebe zu, dass dies ein bisschen Eckfall ist). Zum Beispiel können Sie haben

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

Dies ermöglicht das Hinzufügen des EmployeeMixins zu allem, was eine Unterklasse von Personund ist Expense. Dies ist natürlich nur dann sinnvoll, wenn es Expenseerweitert wird Personoder umgekehrt. Der Punkt ist, dass die Verwendung von Selbsttypen Employeeunabhängig von der Hierarchie der Klassen sein kann, von denen sie abhängt. Es ist egal, was was erweitert - Wenn Sie die Hierarchie von Expensevs wechseln Person, müssen Sie keine Änderungen vornehmen Employee.

Petr Pudlák
quelle
Der Mitarbeiter muss keine Klasse sein, um von der Person abstammen zu können. Eigenschaften können Klassen erweitern. Wenn das Mitarbeitermerkmal Person erweitert, anstatt einen Selbsttyp zu verwenden, würde das Beispiel weiterhin funktionieren. Ich finde Ihr Beispiel interessant, aber es scheint keinen Anwendungsfall für Selbsttypen zu veranschaulichen.
Morgan Creighton
@ MorganCreighton Fair genug, ich wusste nicht, dass Eigenschaften Klassen erweitern können. Ich werde darüber nachdenken, ob ich ein besseres Beispiel finden kann.
Petr Pudlák
Ja, es ist eine überraschende Sprachfunktion. Wenn Merkmal Mitarbeiter erweiterte Klasse Person, dann müsste jede Klasse, die letztendlich in Mitarbeiter "verwelkt" ist, auch Person erweitern. Diese Einschränkung besteht jedoch weiterhin, wenn der Mitarbeiter einen Selbsttyp verwendet, anstatt die Person zu erweitern. Prost, Petr!
Morgan Creighton
1
Ich verstehe nicht, warum "dies nur dann sinnvoll ist, wenn die Kosten die Person verlängern oder umgekehrt".
Robin Green
0

Im ersten Fall kann ein Untermerkmal oder eine Unterklasse von B in jede Verwendung von A eingemischt werden. B kann also ein abstraktes Merkmal sein.

IttayD
quelle
Nein, B kann (und ist) in beiden Fällen ein "abstraktes Merkmal" sein. Aus dieser Perspektive gibt es also keinen Unterschied.
Robin Green