Scala: Abstrakte Typen gegen Generika


Antworten:


257

Sie haben hier einen guten Standpunkt zu diesem Thema:

Der Zweck von Scalas Typensystem
Ein Gespräch mit Martin Odersky, Teil III
von Bill Venners und Frank Sommers (18. Mai 2009)

Update (Oktober 2009): Das Folgende wurde in diesem neuen Artikel von Bill Venners tatsächlich veranschaulicht:
Abstrakte Typmitglieder im Vergleich zu generischen Typparametern in Scala (siehe Zusammenfassung am Ende)


(Hier ist der relevante Auszug aus dem ersten Interview, Mai 2009, Schwerpunkt Mine)

Allgemeines Prinzip

Es gab immer zwei Begriffe von Abstraktion:

  • Parametrierung und
  • abstrakte Mitglieder.

In Java haben Sie auch beides, aber es hängt davon ab, worüber Sie abstrahieren.
In Java haben Sie abstrakte Methoden, aber Sie können keine Methode als Parameter übergeben.
Sie haben keine abstrakten Felder, können jedoch einen Wert als Parameter übergeben.
Ebenso haben Sie keine abstrakten Typelemente, aber Sie können einen Typ als Parameter angeben.
In Java gibt es also auch alle drei, aber es gibt einen Unterschied, welches Abstraktionsprinzip Sie für welche Art von Dingen verwenden können. Und Sie könnten argumentieren, dass diese Unterscheidung ziemlich willkürlich ist.

Der Scala-Weg

Wir haben beschlossen, für alle drei Arten von Mitgliedern die gleichen Konstruktionsprinzipien zu haben .
Sie können also sowohl abstrakte Felder als auch Werteparameter haben.
Sie können Methoden (oder "Funktionen") als Parameter übergeben oder über diese abstrahieren.
Sie können Typen als Parameter angeben oder über sie abstrahieren.
Und was wir konzeptionell bekommen, ist, dass wir eines in Bezug auf das andere modellieren können. Zumindest im Prinzip können wir jede Art von Parametrisierung als eine Form der objektorientierten Abstraktion ausdrücken. In gewissem Sinne könnte man sagen, dass Scala eine orthogonalere und vollständigere Sprache ist.

Warum?

Was Sie insbesondere bei abstrakten Typen kaufen, ist eine gute Behandlung für diese Kovarianzprobleme, über die wir zuvor gesprochen haben.
Ein Standardproblem, das es schon lange gibt, ist das Problem der Tiere und Lebensmittel.
Das Rätsel war, eine Klasse Animalmit einer Methode zu haben eat, die etwas zu essen isst.
Das Problem ist, wenn wir Tier unterordnen und eine Klasse wie Kuh haben, dann essen sie nur Gras und kein willkürliches Essen. Eine Kuh konnte zum Beispiel keinen Fisch essen.
Was Sie wollen, ist zu sagen, dass eine Kuh eine Fressmethode hat, die nur Gras und keine anderen Dinge frisst.
Tatsächlich können Sie dies in Java nicht tun, da sich herausstellt, dass Sie unsolide Situationen konstruieren können, wie das Problem, einer Apple-Variablen, über die ich zuvor gesprochen habe, eine Frucht zuzuweisen.

Die Antwort ist, dass Sie der Animal-Klasse einen abstrakten Typ hinzufügen .
Sie sagen, meine neue Tierklasse hat eine Art von SuitableFood, die ich nicht kenne.
Es ist also ein abstrakter Typ. Sie geben keine Implementierung des Typs an. Dann haben Sie eine eatMethode, die nur isst SuitableFood.
Und dann Cowwürde ich in der Klasse sagen, OK, ich habe eine Kuh, die die Klasse erweitert Animal, und für Cow type SuitableFood equals Grass.
So abstrakte Typen bieten diese Vorstellung von einer Art in einer übergeordneten Klasse , dass ich nicht weiß, was ich füllen dann später in Unterklassen mit etwas , das ich kenne .

Gleiches gilt für die Parametrierung?

In der Tat können Sie. Sie können die Klasse Tier mit der Art des Futters parametrisieren, das sie isst.
Aber in der Praxis, wenn man viele verschiedene Dinge tun, mit, führt es zu einer Explosion von Parametern , und in der Regel, was mehr ist , in Grenzen Parameter .
Auf der ECOOP 1998 hatten Kim Bruce, Phil Wadler und ich ein Papier, in dem wir zeigten, dass das typische Programm quadratisch wächst , wenn Sie die Anzahl der Dinge erhöhen, die Sie nicht kennen .
Es gibt also sehr gute Gründe, keine Parameter zu machen, sondern diese abstrakten Mitglieder zu haben, weil sie Ihnen diese quadratische Explosion nicht geben.


thatismatt fragt in den Kommentaren:

Denken Sie, dass das Folgende eine faire Zusammenfassung ist:

  • Abstrakte Typen werden in 'has-a'- oder' used-a'-Beziehungen verwendet (z. B. a Cow eats Grass).
  • wo als Generika normalerweise 'von' Beziehungen sind (zB List of Ints)

Ich bin mir nicht sicher, ob die Beziehung zwischen der Verwendung abstrakter Typen oder Generika so unterschiedlich ist. Was ist anders ist:

  • wie sie verwendet werden, und
  • wie Parametergrenzen verwaltet werden.

Um zu verstehen, wovon Martin spricht, wenn es um die "Explosion von Parametern und normalerweise darüber hinaus in Grenzen von Parametern " und das anschließende quadratische Wachstum geht, wenn abstrakte Typen mithilfe von Generika modelliert werden, können Sie das Papier " Skalierbare Komponentenabstraktion" betrachten "geschrieben von ... Martin Odersky und Matthias Zenger für OOPSLA 2005, auf die in den Veröffentlichungen des Projekts Palcom (Abschluss 2007) verwiesen wird .

Relevante Auszüge

Definition

Abstrakte Typelemente bieten eine flexible Möglichkeit, über konkrete Komponententypen zu abstrahieren.
Abstrakte Typen können Informationen zu Interna einer Komponente verbergen, ähnlich wie sie in SML- Signaturen verwendet werden. In einem objektorientierten Framework, in dem Klassen durch Vererbung erweitert werden können, können sie auch als flexibles Mittel zur Parametrisierung verwendet werden (häufig als Familienpolymorphismus bezeichnet, siehe beispielsweise diesen Weblog-Eintrag und das von Eric Ernst verfasste Papier ).

(Hinweis: Familienpolymorphismus wurde für objektorientierte Sprachen als Lösung zur Unterstützung wiederverwendbarer, aber typsicherer, gegenseitig rekursiver Klassen vorgeschlagen.
Eine Schlüsselidee des Familienpolymorphismus ist der Begriff der Familien, mit denen sich gegenseitig rekursive Klassen gruppieren.)

begrenzte Typabstraktion

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Hier wird die Typdeklaration von T durch eine obere Typgrenze eingeschränkt, die aus einem geordneten Klassennamen und einer Verfeinerung besteht { type O = T }.
Die Obergrenze beschränkt die Spezialisierungen von T in Unterklassen auf die Untertypen von Ordered, für die das Typmitglied Ovon equals T.
Aufgrund dieser Einschränkung ist die <Methode der Klasse Ordered garantiert auf einen Empfänger und ein Argument vom Typ T anwendbar.
Das Beispiel zeigt, dass das begrenzte Typelement selbst als Teil der Grenze erscheinen kann.
(dh Scala unterstützt F-gebundenen Polymorphismus )

(Anmerkung von Peter Canning, William Cook, Walter Hill und Walter Olthoff: Die begrenzte
Quantifizierung wurde von Cardelli und Wegner als Mittel zur Typisierung von Funktionen eingeführt, die über alle Subtypen eines bestimmten Typs hinweg einheitlich funktionieren.
Sie definierten ein einfaches "Objekt" -Modell und eine begrenzte Quantifizierung verwendet, um Typprüffunktionen zu überprüfen, die für alle Objekte mit einem bestimmten Satz von "Attributen" sinnvoll sind.
Eine realistischere Darstellung objektorientierter Sprachen würde Objekte ermöglichen, die Elemente rekursiv definierter Typen sind .
In diesem Zusammenhang begrenzt Die Quantifizierung erfüllt nicht mehr ihren beabsichtigten Zweck. Es ist leicht, Funktionen zu finden, die für alle Objekte mit bestimmten Methoden sinnvoll sind, die jedoch nicht im Cardelli-Wegner-System eingegeben werden können.
Um eine Grundlage für typisierte polymorphe Funktionen in objektorientierten Sprachen zu schaffen, führen wir eine F-gebundene Quantifizierung ein.

Zwei Gesichter derselben Münzen

In Programmiersprachen gibt es zwei Hauptformen der Abstraktion:

  • Parametrierung und
  • abstrakte Mitglieder.

Die erste Form ist typisch für funktionale Sprachen, während die zweite Form typischerweise in objektorientierten Sprachen verwendet wird.

Traditionell unterstützt Java die Parametrisierung für Werte und die Elementabstraktion für Operationen. Das neuere Java 5.0 mit Generika unterstützt die Parametrisierung auch für Typen.

Es gibt zwei Argumente für die Aufnahme von Generika in Scala:

  • Erstens ist die Codierung in abstrakte Typen nicht so einfach von Hand durchzuführen. Neben dem Verlust an Prägnanz gibt es auch das Problem versehentlicher Namenskonflikte zwischen abstrakten Typnamen, die Typparameter emulieren.

  • Zweitens spielen Generika und abstrakte Typen in Scala-Programmen normalerweise unterschiedliche Rollen.

    • Generika werden normalerweise verwendet, wenn nur eine Typinstanziierung erforderlich ist
    • abstrakte Typen werden normalerweise verwendet, wenn auf den abstrakten Typ aus dem Clientcode verwiesen werden muss .
      Letzteres tritt insbesondere in zwei Situationen auf:
    • Möglicherweise möchten Sie die genaue Definition eines Typmitglieds vor dem Clientcode verbergen, um eine Art Kapselung zu erhalten, die aus Modulsystemen im SML-Stil bekannt ist.
    • Oder man möchte den Typ in Unterklassen kovariant überschreiben, um einen Familienpolymorphismus zu erhalten.

In einem System mit begrenztem Polymorphismus kann das Umschreiben des abstrakten Typs in Generika eine quadratische Erweiterung der Typgrenzen zur Folge haben .


Update Oktober 2009

Abstract Type Members versus generische Typparameter in Scala (Bill Venners)

(Hervorhebung von mir)

Meine bisherige Beobachtung zu abstrakten Typmitgliedern ist, dass sie in erster Linie eine bessere Wahl sind als generische Typparameter, wenn:

  • Sie möchten, dass Personen Definitionen dieser Typen über Merkmale einmischen .
  • Sie denken, dass die explizite Erwähnung des Typmitgliedsnamens bei der Definition die Lesbarkeit des Codes verbessert .

Beispiel:

Wenn Sie drei verschiedene Fixture-Objekte an Tests übergeben möchten, können Sie dies tun, müssen jedoch drei Typen angeben, einen für jeden Parameter. Hätte ich also den Typparameter-Ansatz gewählt, hätten Ihre Suite-Klassen möglicherweise so aussehen können:

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

Beim Typ-Member-Ansatz sieht dies folgendermaßen aus:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Ein weiterer kleiner Unterschied zwischen abstrakten Typelementen und generischen Typparametern besteht darin, dass die Leser des Codes den Namen des Typparameters nicht sehen, wenn ein generischer Typparameter angegeben wird. So sollte jemand diese Codezeile sehen:

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Sie würden nicht wissen, wie der Name des als StringBuilder angegebenen Typparameters lautete, ohne ihn nachzuschlagen. Während der Name des Typparameters genau dort im Code des abstrakten Typelementansatzes steht:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

Im letzteren Fall könnten Leser des Codes erkennen, dass StringBuilderes sich um den Typ "Fixture Parameter" handelt.
Sie müssten immer noch herausfinden, was "Fixture-Parameter" bedeutet, aber sie könnten zumindest den Namen des Typs erhalten, ohne in der Dokumentation nachzuschauen.


61
Wie soll ich Karma-Punkte bekommen, indem ich Scala-Fragen beantworte, wenn du kommst und das tust ??? :-)
Daniel C. Sobral

7
Hallo Daniel, ich denke, es muss konkrete Beispiele geben, um die Vorteile abstrakter Typen gegenüber der Parametrisierung zu veranschaulichen. Einige in diesem Thread zu posten wäre ein guter Anfang;) Ich weiß, ich würde das positiv bewerten.
VonC

1
Denken Sie, dass das Folgende eine faire Zusammenfassung ist: Abstrakte Typen werden in 'Has-a'- oder' Uses-a'-Beziehungen verwendet (z. B. eine Kuh frisst Gras), wobei Generika normalerweise 'of'-Beziehungen sind (z. B. List of Ints)
thatismatt

Ich bin mir nicht sicher, ob die Beziehung zwischen der Verwendung abstrakter Typen oder Generika so unterschiedlich ist. Was anders ist, ist, wie sie verwendet werden und wie Parametergrenzen verwaltet werden. Mehr in meiner Antwort gleich.
VonC

1
Hinweis für sich selbst: siehe auch diesen Blog-Beitrag vom Mai 2010: daily-scala.blogspot.com/2010/05/…
VonC

37

Ich hatte die gleiche Frage, als ich über Scala las.

Der Vorteil der Verwendung von Generika besteht darin, dass Sie eine Typenfamilie erstellen. Niemand wird zu Unterklasse müssen Buffer-sie kann nur nutzen Buffer[Any], Buffer[String]usw.

Wenn Sie einen abstrakten Typ verwenden, müssen Benutzer eine Unterklasse erstellen. Die Menschen werden Klassen müssen wie AnyBuffer, StringBufferusw.

Sie müssen entscheiden, welches für Ihren speziellen Bedarf besser ist.


18
mmm dünner hat sich an dieser Front stark verbessert, Sie können nur verlangen Buffer { type T <: String }oder Buffer { type T = String }je nach Ihren Bedürfnissen
Eduardo Pareja Tobes

21

Sie können abstrakte Typen in Verbindung mit Typparametern verwenden, um benutzerdefinierte Vorlagen einzurichten.

Nehmen wir an, Sie müssen ein Muster mit drei miteinander verbundenen Merkmalen erstellen:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

in der Weise, dass die in Typparametern erwähnten Argumente respektvoll AA, BB, CC selbst sind

Sie können mit einer Art Code kommen:

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

was aufgrund von Typparameterbindungen nicht auf diese einfache Weise funktionieren würde. Sie müssen es kovariant machen, um richtig zu erben

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

Dieses eine Beispiel würde kompiliert, stellt jedoch hohe Anforderungen an Varianzregeln und kann in einigen Fällen nicht verwendet werden

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

Der Compiler wird mit einer Reihe von Abweichungsprüfungsfehlern Einwände erheben

In diesem Fall können Sie alle Typanforderungen in zusätzlichen Merkmalen erfassen und andere Merkmale darüber parametrisieren

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

Jetzt können wir eine konkrete Darstellung für das beschriebene Muster schreiben, Links- und Verknüpfungsmethoden in allen Klassen definieren und rechts und doppelt kostenlos erhalten

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

Daher werden sowohl abstrakte Typen als auch Typparameter zum Erstellen von Abstraktionen verwendet. Sie haben beide Schwachstellen und Stärken. Abstrakte Typen sind spezifischer und können jede Typstruktur beschreiben, sind jedoch ausführlich und müssen explizit angegeben werden. Typparameter können sofort eine Reihe von Typen erstellen, sorgen jedoch zusätzlich für Vererbung und Typgrenzen.

Sie geben sich gegenseitig Synergien und können zusammen verwendet werden, um komplexe Abstraktionen zu erstellen, die nicht mit nur einer von ihnen ausgedrückt werden können.


0

Ich denke, dass es hier keinen großen Unterschied gibt. Typ abstrakte Elemente können als existenzielle Typen angesehen werden, die Datensatztypen in einigen anderen funktionalen Sprachen ähneln.

Zum Beispiel haben wir:

class ListT {
  type T
  ...
}

und

class List[T] {...}

Dann ListTist es genauso List[_]. Die Überzeugung von Typmitgliedern ist, dass wir Klassen ohne expliziten konkreten Typ verwenden und zu viele Typparameter vermeiden können.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.