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 Animal
mit 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 eat
Methode, die nur isst SuitableFood
.
Und dann Cow
wü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 O
von 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 StringBuilder
es 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.