Programmiersprachen-Design, Teil 2: Ein alternatives Sprachdesign für Gleichheit

Die Unterscheidung von Gleichheit und Identität gehört zu den Grundfesten der Programmierung. Doch diese Einteilung und ihre Umsetzung in vielen Programmiersprachen hat ihre Schwächen. Hier setzt ein alternativer Ansatz, die Unterscheidung von Übereinstimmung und Ähnlichkeit, einen anderen Schnitt.

In Pocket speichern vorlesen Druckansicht 21 Kommentare lesen
Programmiersprachen-Design, Teil 2: Ein alternatives Sprachdesign für Gleichheit
Lesezeit: 15 Min.
Von
  • Beate Ritterbach
Inhaltsverzeichnis

Dabei handelt es sich nicht lediglich um eine Umbenennung, sondern um eine grundlegend andere Einteilung: Übereinstimmung umfasst, abhängig von der Art der verglichenen Gegenstände, sowohl Identität bei Objekten als auch Gleichheit bei Werten. Ähnlichkeit dagegen beschreibt eine andere, vorwiegend bei Objekten anzutreffende Art von "Gleichheit".

Mehr Infos

Bei der Unterscheidung verschiedener Gleichheiten geht es nicht um "richtig" oder "falsch", sondern darum, wie sich mit der jeweiligen Sichtweise arbeiten lässt. Das lässt sich anhand des historischen Beispiels von geozentrischem und heliozentrischem Weltbild veranschaulichen: Weil Körper sich relativ zueinander bewegen, kann man sowohl die Erde als auch die Sonne als Bezugspunkt wählen. Relativ zur Erde vollführen die anderen Planeten komplexe, verschnörkelte Bewegungen. Mit der Sonne als Bezugspunkt werden die Planetenbahnen zu Ellipsen und damit zu einfachen, mathematisch gut beschreibbaren Gebilden. Deshalb hatte das heliozentrische Weltbild das geozentrische abgelöst: Es erlaubt in vielerlei Hinsicht eine einfachere Handhabung.

Dieser Beitrag will zeigen, welche Vereinfachungen die Unterscheidung von Übereinstimmung und Ähnlichkeit ermöglicht. Angenommen, es soll eine neue Programmiersprache entworfen werden. Wie lassen sich, ausgehend von den obigen Überlegungen, Übereinstimmung und Ähnlichkeit gestalten?

Übereinstimmung wird seitens der Sprache einheitlich unterstützt. Alle Typen verwenden für sie dasselbe Symbol, unabhängig davon, ob Objekte oder Werte verglichen werden, und unabhängig davon, wie Übereinstimmung implementierungstechnisch umgesetzt ist. Es macht die Programmiersprache klarer und aussagekräftiger, für dieselbe Bedeutung überall dasselbe Symbol zu verwenden.

Als ein geeigneter Kandidat bietet sich das Gleichheitszeichen an, denn es wird in der Mathematik seit langem in genau dieser Bedeutung verwendet. Diese Wahl kollidiert mit der in Java, C# und anderen syntaktisch von C abgeleiteten Sprachen verwendeten Notation. Sie haben das Gleichheitszeichen "okkupiert", für die Zuweisung zweckentfremdet und müssen deshalb für das Vergleichen ein anderes Symbol, nämlich "==", verwenden. Eingängiger und in etlichen anderen Sprachen üblich ist das Symbol "=" für das Vergleichen und ":=" für die Zuweisung.

Ähnlichkeit erhält keine dedizierte Unterstützung durch die Programmiersprache, kein eigenes Symbol, keine spezielle Methode (wie equals). Ein Grund für diese Zurückhaltung: Ähnlichkeit hat keine einheitliche Bedeutung. Klar ist nur, worin sie sich von Übereinstimmung unterscheidet: Bei Ähnlichkeit liegen zwei Exemplare vor, bei Übereinstimmung ein einziges. Was dagegen Ähnlichkeit kennzeichnet, ist vage und hängt von den jeweiligen Anforderungen ab. Wann sind zum Beispiel zwei Bücher "gleich"? Genügen Autor und Titel? Oder fließen auch Verlag und Auflage ein? Spielt es eine Rolle, wenn das eine Buch einen festen Einband hat und das andere als Taschenbuch vorliegt? Welche Merkmale sind im Allgemeinen relevant dafür, zwei Objekte als "gleich" anzusehen?

Ein weiterer Grund: Anders als Übereinstimmung ist Ähnlichkeit kein universelles Konzept. Für viele Klassen wird ein derartiger Vergleich gar nicht benötigt. Umgekehrt kann es Klassen geben, die mehrere Ähnlichkeiten brauchen. Ähnlichkeit ist eine fachliche Operation wie jede andere. Es ergibt keinen Sinn, sie mit einem speziellen Sprachprimitiv zu unterstützen. Es erwartet auch niemand, dass fachliche Operationen wie "Mehrwertsteuer berechnen" oder "Schadenhöhe ermitteln" in der Sprache vordefiniert sind.

Ähnlichkeit nicht als in der Sprache vordefinierte Operation zu unterstützen, ist deshalb kein Mangel, sondern hat viele Vorteile:

  • Wenn eine Klasse keine Ähnlichkeit benötigt, ist sie auch nicht standardmäßig vorhanden und kann damit keine Verwirrung stiften.
  • Ähnlichkeit lässt sich, wenn eine Klasse sie benötigt, als "normale" Methode implementieren – mit einem passenden Namen und einer geeigneten Signatur. Weil sie damit einen anderen Namen beziehungsweise ein anderes Symbol als Übereinstimmung erhält, können die beiden Vergleiche nicht miteinander verwechselt oder vermengt werden.
  • Benötigt eine Klasse mehrere Ähnlichkeiten, können Programmierer dafür mehrere Methoden definieren: unter verschiedenen Namen, die die Besonderheiten der jeweiligen Vergleiche zum Ausdruck bringen.

Es gibt einen dritten Grund, Ähnlichkeit weder durch die Programmiersprache zu unterstützen noch ihr Vorhandensein für jede Klasse zu verlangen: Sie ist fehleranfällig. Eine Gleichheit muss eine Reihe von Bedingungen einhalten (beschrieben z. B. von Odersky, Spoon und Venners). Um einige kritische Punkte herauszugreifen:

  1. Sie muss eine Äquivalenzrelation bilden (d. h. reflexiv, symmetrisch und transitiv sein). Insbesondere die Transitivität ist programmiertechnisch schwer umzusetzen.
  2. Sie darf nicht von änderbarem Zustand abhängen.
  3. Es sind Abhängigkeiten zu Methoden wie hashcode zu berücksichtigen.

Verstöße gegen diese Regeln resultieren in überraschendem, indeterministischem Programmverhalten und schwer auffindbaren Laufzeitfehlern (eingehend beschrieben im eben genannten Blog von Odersky, Spoon und Venners).

Bei Übereinstimmung sind die genannten Regeln fast immer automatisch erfüllt, da es sich hier per Definition um ein einziges Exemplar (Objekt bzw. Wert) handelt. Bei Ähnlichkeit dagegen ist die Gefahr besonders groß, gegen die Regeln zu verstoßen:

  • zu 1.: Wenn eine Ähnlichkeit kleine Abweichungen toleriert, das heißt, wenn sie zwei Objekte bei nur geringfügigen Abweichungen ihrer relevanten Merkmale als "gleich" einstuft, können sich diese Abweichungen aufaddieren und die Transitivität zerstören.
  • zu 2.: Ähnlichkeit basiert, anders als Übereinstimmung, auf "relevanten" Merkmalen. Diese Merkmale sind Bestandteil des Objektzustands und damit im Prinzip änderbar.
  • zu 3.: Es kann Merkmale eines Objekts geben, die für Ähnlichkeit nicht relevant sind. Wenn sie in die Ermittlung des hashcode einfließen, können solche Objekte einen unterschiedlichen hashcode haben, mit entsprechenden Konsequenzen in hashcode-basierten Kontexten, etwa in vielen Collections.

Im Ergebnis kommt ein Sprachdesign heraus, das nur einen einzigen Vergleich unterstützt: Übereinstimmung. In heutigen Programmiersprachen und Anwendungssystemen finden sich bereits einige Indizien dafür, dass ein einziger Vergleich genügt:

  • Für etliche Typen gibt es von vornherein nur einen einzigen Vergleich. Zum Beispiel lassen sich Basisdatentypen in Java (int, float, char, boolean etc.) nur mit dem Operator "==" vergleichen (der die Wertgleichheit abbildet), nicht mit equals.
  • Für Klassen, bei denen mehrere Vergleiche zur Verfügung stehen, kommt meist trotzdem nur ein einziger davon praktisch zum Einsatz. In Java werden beispielsweise Exemplare der Klassen wie String, Date oder BigInteger nahezu immer mit der Methode equals verglichen. Anwendungsbeispiele, bei denen "==" zum Einsatz kommt (und korrekt ist!), muss man fast schon gewaltsam konstruieren.
  • Für Klassen wie Person, Fahrzeug, Vertrag oder Abteilung dagegen ist in der Regel "==" der fachlich sinnvolle Vergleich. Sie lassen sich zwar auch mit equals vergleichen, doch ist diese Methode meist lediglich ein Wrapper für "==" und kommt vorwiegend in polymorphen Kontexten zum Einsatz.