Ein gutes generisches Typensystem

29

Es ist allgemein anerkannt, dass Java-Generika auf einige wichtige Arten versagt haben. Die Kombination von Platzhaltern und Schranken führte zu ernsthaft unlesbarem Code.

Wenn ich mir jedoch andere Sprachen anschaue, kann ich anscheinend kein generisches Typsystem finden, mit dem Programmierer zufrieden sind.

Nehmen wir als Entwurfsziele eines solchen Typsystems:

  • Erzeugt immer leicht lesbare Typdeklarationen
  • Einfach zu erlernen (Kovarianz, Kontravarianz usw. müssen nicht aufgefrischt werden)
  • Maximiert die Anzahl der Fehler bei der Kompilierung

Gibt es eine Sprache, die es richtig macht? Wenn ich google, sehe ich nur Beschwerden darüber, wie das Typsystem in der Sprache X funktioniert. Ist diese Komplexität mit der generischen Typisierung verbunden? Sollten wir einfach aufhören, die Typensicherheit bei der Kompilierung zu 100% zu überprüfen?

Meine Hauptfrage ist, in welcher Sprache diese drei Ziele am besten erreicht wurden. Mir ist klar, dass das subjektiv ist, aber ich kann noch nicht einmal eine Sprache finden, in der nicht alle Programmierer der Meinung sind, dass das generische Typensystem ein Chaos ist.

Nachtrag: Wie bereits erwähnt, ist die Kombination von Subtypisierung / Vererbung und Generika das, was die Komplexität erzeugt. Daher suche ich wirklich nach einer Sprache, die beides kombiniert und die Explosion der Komplexität vermeidet.

Peter
quelle
2
Was meinst du damit easy-to-read type declarations? Das dritte Kriterium ist ebenfalls nicht eindeutig: Beispielsweise kann ich Array-Index-Ausnahmen außerhalb der Grenzen in Fehler bei der Kompilierung umwandeln, indem Sie keine Arrays indexieren lassen, es sei denn, ich kann den Index zur Kompilierungszeit berechnen. Das zweite Kriterium schließt auch die Untertypisierung aus. Das ist nicht unbedingt eine schlechte Sache, aber Sie sollten sich dessen bewusst sein, was Sie fragen.
Doval
9
@gnat, das ist definitiv kein Scherz gegen Java. Ich programmiere fast ausschließlich in Java. Mein Punkt ist, dass es in der Java-Community allgemein anerkannt ist, dass Generics fehlerhaft sind (kein vollständiger Fehler, aber wahrscheinlich ein teilweiser). Daher ist es eine logische Frage, zu fragen, wie sie hätten implementiert werden sollen. Warum sind sie falsch und haben andere sie richtig gemacht? Oder ist es tatsächlich unmöglich, die richtigen Generika zu finden?
Peter
1
Würden alle nur von C # stehlen, gäbe es weniger Beschwerden. Insbesondere Java ist in der Lage, durch Kopieren aufzuholen. Stattdessen entscheiden sie sich für minderwertige Lösungen. Viele der Fragen, die die Java-Design-Komitees noch diskutieren, wurden bereits in C # entschieden und implementiert. Sie scheinen nicht einmal zu schauen.
USR
2
@emodendroket: Ich denke, meine beiden größten Beschwerden über C # -Generika sind, dass es keine Möglichkeit gibt, eine "Supertype" -Einschränkung anzuwenden (z. B. Foo<T> where SiameseCat:T), und dass es keine Möglichkeit gibt, einen generischen Typ zu haben, der nicht konvertierbar ist Object. Meiner Meinung nach würde .NET von Aggregattypen profitieren, die strukturähnlich, aber noch bloßer sind. Wenn KeyValuePair<TKey,TValue>es sich um einen solchen Typ handelt, kann auf einen umgewandelt IEnumerable<KeyValuePair<SiameseCat,FordFocus>>werden IEnumerable<KeyValuePair<Animal,Vehicle>>, jedoch nur, wenn der Typ nicht in ein Kästchen eingeschlossen werden kann.
Supercat

Antworten:

24

Während Generika seit Jahrzehnten ein fester Bestandteil der Community der funktionalen Programmierung sind, bietet das Hinzufügen von Generika zu objektorientierten Programmiersprachen einige einzigartige Herausforderungen, insbesondere das Zusammenspiel von Untertypen und Generika.

Selbst wenn wir uns auf objektorientierte Programmiersprachen und insbesondere auf Java konzentrieren, hätte ein weitaus besseres generisches System entworfen werden können:

  1. Generische Typen sollten überall dort zulässig sein, wo es andere Typen gibt. Insbesondere wenn Tes sich um einen Typparameter handelt, sollten die folgenden Ausdrücke ohne Warnungen kompiliert werden:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Ja, dazu müssen Generika wie jeder andere Typ in der Sprache angepasst werden.

  2. Kovarianz und Kontravarianz eines generischen Typs sollten in seiner Deklaration angegeben (oder daraus abgeleitet) werden, anstatt jedes Mal, wenn der generische Typ verwendet wird, damit wir schreiben können

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    eher, als

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Da generische Typen ziemlich lang werden können, sollten wir sie nicht redundant angeben müssen. Das heißt, wir sollten schreiben können

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    eher, als

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Als Typparameter sollte jeder Typ zulässig sein, nicht nur Referenztypen. (Wenn wir eine haben können int[], warum können wir keine haben List<int>)?

All dies ist in C # möglich.

Meriton - im Streik
quelle
1
Würde dies auch selbstreferentielle Generika loswerden? Was ist, wenn ich sagen möchte, dass ein vergleichbares Objekt sich mit etwas vom selben Typ oder einer Unterklasse vergleichen kann? Geht das Oder wenn ich eine Sortiermethode schreibe, die Listen mit vergleichbaren Objekten akzeptiert, müssen alle miteinander vergleichbar sein. Enum ist ein weiteres gutes Beispiel: Enum <E erweitert Enum <E >>. Ich sage nicht, dass ein Typsystem in der Lage sein sollte, dies zu tun. Ich bin nur neugierig, wie C # mit diesen Situationen umgeht.
Peter
1
Die generische Typinferenz von Java 7 und die automatische Hilfe von C ++ bei einigen dieser Probleme sind jedoch syntaktischer Zucker und ändern die zugrunde liegenden Mechanismen nicht.
Die Typinferenz von @Snowman Java weist jedoch einige wirklich unangenehme Eckfälle auf, zum Beispiel, dass Sie überhaupt nicht mit anonymen Klassen arbeiten und die richtigen Grenzen für Platzhalter nicht finden, wenn Sie eine generische Methode als Argument für eine andere generische Methode auswerten.
Doval
@Doval Deshalb habe ich gesagt, es hilft bei einigen Problemen: Es behebt nichts und geht nicht auf alles ein. Java-Generika haben viele Probleme: Sie sind zwar besser als RAW-Typen, verursachen jedoch mit Sicherheit viele Kopfschmerzen.
34

Die Verwendung von Subtypen verursacht beim generischen Programmieren viele Komplikationen. Wenn Sie darauf bestehen, eine Sprache mit Untertypen zu verwenden, müssen Sie akzeptieren, dass die damit einhergehende generische Programmierung eine gewisse Komplexität aufweist. Einige Sprachen können es besser als andere, aber Sie können es nur so weit bringen.

Vergleichen Sie das zum Beispiel mit Haskells Generika. Sie sind einfach genug , dass , wenn Sie Typinferenz verwenden, können Sie eine korrekte generische Funktion von Schreib Unfall . In der Tat, wenn Sie einen einzigen Typ angeben, oft der Compiler selbst sagt : „Nun, ich war im Begriff , dieses generisch zu machen, aber man hat mich gebeten , es für ints nur zu machen, so was auch immer.“

Zugegeben, die Leute verwenden Haskells Schriftsystem auf erstaunlich komplexe Weise, was es zum Fluch eines jeden Neulings macht, aber das zugrunde liegende Schriftsystem selbst ist elegant und sehr bewundert.

Karl Bielefeldt
quelle
1
Danke für diese Antwort. Dieser Artikel beginnt mit einigen Beispielen von Joshua Bloch, bei denen Generika zu kompliziert werden: artima.com/weblogs/viewpost.jsp?thread=222021 . Ist dies ein Unterschied in der Kultur zwischen Java und Haskell, wo solche Konstrukte in Haskell als gut angesehen würden, oder gibt es einen wirklichen Unterschied zu Haskells Typsystem, das solche Situationen vermeidet?
Peter
10
@Peter Haskell hat keine Untertypisierung, und wie Karl sagte, kann der Compiler die Typen automatisch ableiten, einschließlich Einschränkungen wie "Typ amuss eine Art Ganzzahl sein".
Doval
Mit anderen Worten Kovarianz , in Sprachen wie Scala.
Paul Draper
14

Vor etwa 20 Jahren wurde eine ganze Reihe von Forschungen angestellt, um Generika mit Subtypisierung zu kombinieren. In der Programmiersprache Thor, die von Barbara Liskovs Forschungsgruppe am MIT entwickelt wurde, gab es "where" -Klauseln, mit denen Sie die Anforderungen des Typs angeben können, über den Sie parametrisieren. (Dies ähnelt dem, was C ++ mit Concepts versucht .)

Der Artikel, der Thors Generika beschreibt und wie sie mit Thors Untertypen interagieren, lautet: Day, M; Gruber, R; Liskov, B; Myers, AC: Subtypes vs. where-Klauseln: Einschränkung des parametrischen Polymorphismus , ACM Conf on Obj-Oriented Prog, Sys, Lang und Apps , (OOPSLA-10): 156-158, 1995.

Ich glaube, dass sie ihrerseits auf Arbeiten aufbauen, die Ende der 1980er Jahre an Emerald ausgeführt wurden. (Ich habe diese Arbeit nicht gelesen, aber die Referenz ist: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Verteilung und abstrakte Typen in Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Sowohl Thor als auch Emerald waren "akademische Sprachen", so dass sie wahrscheinlich nicht genug gebraucht wurden, um wirklich zu verstehen, ob Where-Klauseln (Konzepte) wirklich echte Probleme lösen. Es ist interessant, Bjarne Stroustrups Artikel darüber zu lesen, warum der erste Versuch mit Concepts in C ++ fehlgeschlagen ist: Stroustrup, B: Die C ++ 0x-Entscheidung "Konzepte entfernen" , Dr. Dobbs , 22. Juli 2009. (Weitere Informationen auf der Homepage von Stroustrup . )

Eine andere Richtung, die die Leute zu versuchen scheinen, nennt man Eigenschaften . Zum Beispiel verwendet Mozillas Programmiersprache Rust Merkmale. So wie ich es verstehe (was möglicherweise völlig falsch ist), ist die Feststellung, dass eine Klasse ein Merkmal erfüllt, fast so, als würde man sagen, dass eine Klasse eine Schnittstelle implementiert. Es scheint, dass Apples neue Swift-Programmiersprachen ein ähnliches Konzept von Protokollen verwenden , um Einschränkungen der Parameter für Generika festzulegen .

Wandering Logic
quelle