 Herzlich willkommen zurück zu Programmierparadigmen. Wir sind hier in Teil 4 von 6 im Modul-Typ-Systeme und in diesem 4. Teil geht es um die Frage, wann Typen überhaupt equivalent sind. Diese Frage ist wichtig, um festzustellen, ob ein Programm überhaupt typkurekt ist. Denn um das zu wissen, müssen wir erst mal herausfinden, ob zwei Typen, die vielleicht nicht gleich heißen oder vielleicht auch ein bisschen anders aufgebaut sind, tatsächlich equivalent sind. Also diese Frage der Typ-Equivalenz ist wie gesagt wichtig, um festzustellen, ob ein Programm typkurekt ist. Und es gibt im Prinzip zwei Arten und Weisen, wie das üblicherweise gemacht wird, die wir uns hier auch beide ein bisschen genauer anschauen werden. Das eine ist die strukturelle Equivalenz, wo wir sagen, dass zwei Typen, wenn zwei Typen dieselbe Struktur haben, sie dann auch equivalent sind. Das heißt, wir schauen nicht an, wie diese Typen eigentlich heißen, sondern schauen uns tatsächlich an, was ist in den Typen drin, welche Felder haben die zum Beispiel. Und wenn das gleich ist, dann ist es auch derselbe Typ. Die andere Art, die Frage, ob zwei Typen equivalent sind, zu beantworten, ist, indem wir uns die Namen der Typen anschauen, wo wir ganz einfach sagen, wenn zwei Typen denselben Namen haben, dann ist das auch derselbe Typ. Und ansonsten aber eben nicht, also selbst wenn die Struktur vielleicht gleich ist, das Ganze aber anders heißt, dann ist es eben nicht derselbe bzw. kein equivalenter Typ. Also schauen wir uns diese beiden Arten und Weisen Equivalenz zu definieren, mal ein bisschen genauer an und fangen mit der strukturellen Equivalenz an. Also wie gesagt ist hier die Idee, dass zwei Typen verglichen werden, indem wir die Struktur vergleichen und genau genommen wird das Ganze rekursiv gemacht. Das heißt, wir fangen von der Wurzel dieser Typstruktur an und gehen dann rekursiv immer tiefer und schauen, ob das tatsächlich gleich ist. Und wenn ja, dann sagen wir, dass die Typen strukturell equivalent sind. Also zum Beispiel würden wir sagen, dass jede Klasse, die diese drei Eigenschaften hier erfüllt, strukturell equivalent zueinander sind. Das heißt, und diese drei Eigenschaften sind, dass es ein Integer-Feld gibt, was zum Beispiel Age heißt, dass es ein Boolean-Feld gibt, was zum Beispiel is a Registrate heißt und dass diese Klasse eine Methode anbietet, die PrintRecord heißt. Und wir würden jede Klasse, egal wie die Klasse jetzt heißt, die diese drei Eigenschaften erfüllt, dann als strukturell equivalent mit anderen Klassen betrachten, die eben auch diese Eigenschaften haben. In Programmi sprachen, die diese Art von struktureller Equivalenz verwenden, gibt es dann natürlich wieder gewisse Variationen und zwei Dimensionen, wo Sprachen voneinander abweichen, sind die Folgenden. Also zum ein ist da die Frage, ob die Namen und inwiefern die Namen denn eine Rolle spielen. Wenn ich jetzt zum Beispiel zwei Klassen habe, die genau dieselbe Repräsentation im Speicher haben, wo aber zum Beispiel die Felder, die in innerhalb dieser Klasse existieren, verschiedene Namen haben, ist die Frage, ob das dann immer noch strukturelle Equivalenz bedeutet. Man kann sagen ja, man kann sagen nein, hängt ganz einfach von der Sprache ab und verschiedene Sprachen geben da verschiedene Antworten. Eine andere Frage, wo die Antwort von der Sprache abhängt, ist, ob die Reihenfolge der Felder oder die Reihenfolge der Dinge, die in diesem Typ repräsentiert sind, eine Rolle spielt. Zum Beispiel ist das wichtig, wenn es um die Reihenfolge von Feldern geht und das spielt insbesondere dann eine Rolle, wenn man den Wert des einen Typs ohne Verluste von Informationen in ein Wert des anderen Typen überführen könnte, wenn man einfach die Reihenfolge der Felder verändert und in manchen Sprachen ist das okay und diese Typen gelten dann als Equivalent. In anderen Sprachen ist das dann aber auch nicht der Fall. Schauen wir uns das Ganze mal mithilfe von ein paar Beispielen genauer an. Und zwar verwende ich in diesen Beispielen eine paskallartige Sündtags, um Typen aufzuschreiben, ganz einfach, weil die Sündtags relativ kompakt ist und auch um so ein bisschen die Vielfalt von Programmiersprachen zu zeigen. Und was ich mit dieser Sündtags jetzt mache, ist einfach mal verschiedene Typen und deren Struktur aufzuschreiben. Also ein Typ, den wir hier definieren, ist ein Record, in dem es mehrere Felder gibt, nämlich zum einen ein Feld namens A vom Typ Integer und dann ein Feld vom Typ Real namens B und das war es dann auch und dann habe ich noch zwei andere Typen, nämlich einen anderen Record, in dem ich ein Feld namens C habe, was auch vom Typ Integer ist und ein Feld namens D, was wiederum vom Typ Real ist und der dritte Typ sieht so ein bisschen ähnlich aus, aber irgendwie auch wieder ganz anders, denn hier habe ich zuerst ein Feld namens B vom Typ Real und dann ein Feld namens A vom Typ Integer und die Frage ist jetzt, inwiefern sind diese Typen equivalent, wenn wir strukturelle Equivalenz anschauen und das hängt nun eben genau von der Sprache ab, also Entsprachen, wo sowohl die Reihenfolge als auch die Namen übereinstimmen müssen, während all diese Typen nicht miteinander kompatibel, in Sprachen, wo die Reihenfolge stimmen muss, aber die Namen der Felder sind egal, während Typ 1 und Typ 2 miteinander kompatibel und wenn die Reihenfolge dann auch egal ist, dann wären sogar alle drei Typen miteinander kompatibel, bald schlussendlich ja alle drei Typen ein Integer und ein Real Feld haben und man könnte jedes Objekt oder jeden Wert vom Typ T3 ohne Probleme in ein Wert vom Typ T2 oder T1 umwandeln, indem man einfach die beiden Felder vertauscht und dann hätte man ein Objekt, was kompatibel mit diesen anderen Typen wäre. Noch ein bisschen interessanter wird es, wenn ich in diesen Records auch noch Pointer auf andere Typen habe und um das zu zeigen, schauen wir einfach mal noch drei Beispiele an, und zwar ein Typ, nennen wir ihn mal T, der auch wieder ein Record ist, indem ich zum einen ein Feld namens Info habe, was ein Integer speichert und zum anderen ein Feld namens Next, was ein Pointer, also dieses kleine Carrot hier, dieses Hütchen, zeigt einfach nur an, dass es ein Pointer ist, auf denselben Typen T wieder hat. Also das könnte zum Beispiel für die Implementierung einer verlinkten Liste verwendet werden. Das ist jetzt der eine Typ, der quasi sagt, ok, ich habe ein Integer und ein Pointer auf andere Objekte meines selben Types und jetzt sag mal mal, wir haben noch zwei weitere Typen, nämlich einen, der auch wieder ein Integer Feld hat, namens Info und dann aber sagt, es gibt einen Pointer auf einen anderen Typen, nämlich ein Typ V und dieser Typ V ist dann hier unten definiert und was wir hier haben, ist auch wieder dieses Infofeld und die Information, dass es da noch ein Feld gibt, das aber jetzt ein Pointer auf ein Wert vom Typ U enthält. Das heißt, diese beiden Typen hier unten referenzieren sich sozusagen gegenseitig und sagen, ich habe ein Pointer in meinem Next Feld und dieser Pointer geht auf ein anderes Objekt und zwar jeweils von dem anderen Typen und die Frage ist jetzt, sind Objekte oder ist der Typ T strukturell equivalent zu den Typen U und V und die Beantwortung dieser Frage ist relativ kompliziert, weil eben U und V ja zwei verschiedene Typen sind, die sie gegenseitig referenzieren und je nachdem, wie die Sprache definiert ist, könnte man das jetzt hier mit ja oder eben auch mit nein beantworten. So, jetzt wissen Sie ein bisschen, was strukturelle Equivalenz eigentlich ist. Jetzt schauen wir uns mal an, ob das vielleicht auch gewisse Probleme mit sich führt. So wie jede Idee gibt es dann natürlich Vor- und Nachteile und der größte Nachteil bei Typesystemen, die strukturelle Equivalenz verwenden, ist, dass wir nicht unbedingt zwischen verschiedenen Konzepten unterscheiden können, wenn diese Konzepte auf die gleiche Art und Weise im Speicher repräsentiert werden. Also hier unten ist mal ein Beispiel, wo wir zwei Typen definieren, wieder mit dieser Pascalartigen Sündtags, nämlich einmal diesen Typ Student, der drei Felder hat, nämlich einen Namen und eine Adresse vom Typ String und ein Feld namens Age vom Typ Integer. Und dann sehen wir hier auf der anderen Seite noch einen Typen, der School heißt und dieselben Felder vom selben Typ und mit denselben Namen hat, also auch wieder den Namen, die Adresse und das Alter, weil das sind ja alles Eigenschaften, die auch eine Schule oder eine Uni haben könnte, aber eben doch eigentlich ein anderes Konzept repräsentiert, nämlich eben eine Schule oder Uni und kein Studenten. Wenn ich jetzt aber eine Sprache habe, in der Typ Equivalenz basierend auf der Struktur der Typen definiert ist, dann kann ich diese Typen miteinander vermischen. Das heißt, ich kann zum Beispiel sowas aufschreiben wie hier, wo ich sage, es gibt da eine Variable vom Typ X, die soll ein Studenten repräsentieren und eine Variable namens Y, die eine Schule oder Uni repräsentieren soll. Und ich kann jetzt aber, weil diese beiden Typen ja zueinander Equivalenz sind, diese Schule in diesen Studenten schreiben. Das heißt, ich kann diese Typen einfach miteinander vermischen und das ist in der Regel nicht das, was man hier möchte, denn obwohl die beiden Konzepte im Speicher gleich repräsentiert sind, sind sie ja eigentlich verschiedene Sachen und sollten eben nicht miteinander vermischt werden. Eben weil strukturelle Equivalenz diese Probleme hat, die wir da gerade an dem Beispiel gesehen haben, gibt es diese andere Art, Equivalenz von Typen zu definieren, nämlich mittels der Namen. Die Idee hier ist, dass wenn zwei Typen verschiedene Namen haben, sie dann einfach verschieden sind. Und die Annahme ist, wenn der Programmierer diese Typen verschieden nennt, na ja, dann will der ja auch, dass das verschiedene Sachen sind und dann sind das eben nicht dieselben Typen. Und dieses Konzept der Name Equivalenz ist das, was in den meisten modernen Sprachen oder zumindest in vielen modernen Sprachen verwendet wird, zum Beispiel in Java, wo der Name einer Klasse eben festlegt, okay, das ist der eine Typ und wenn da eine andere Klasse ist, die vielleicht genau dieselbe Struktur hat und genau dieselben Felder, vielleicht sogar mit denselben Namen und in derselben Reihenfolge hat, dann ist das trotzdem eine andere Klasse, die einen anderen Namen hat und von daher ein anderer Typ. Wie mit jeder Idee, die es irgendwo ein Programmiersprachen gibt, ist die natürlich auch nicht perfekt, sondern auch diese namensbasierte Equivalenz hat gewisse Schwachstellen oder gewisse Problematiken. Und eine davon kommt zutage, wenn man sich alias Types anschaut, also sozusagen die Möglichkeit einen Typ noch einen zweiten Namen zu geben, einen alias, um einfach mit verschiedenen Namen auf denselben Typen verweisen zu können. Hier unten sind mal zwei Beispiele, wo man einmal eben genau möchte, dass diese Typen Equivalenz sind und einmal das wahrscheinlich nicht möchte. In dem ersten Beispiel sagen wir, dass es einen Typ namens Stack Element gibt, der einfach ein Integer ist. Also wir legen fest, für unseren Stack sind die Elemente Integer und aus welchem Grund auch immer geben wir diesen Integer noch einen bestimmten Namen und nennen das einfach Stack Element. Wenn wir jetzt aber zum Beispiel dann so ein Stack haben und jemand da einen Integer drin speichern möchte, dann sollte das eigentlich funktionieren. Das heißt, wir wollen eigentlich, dass Stack Element und Integer Equivalenz sind. Im zweiten Beispiel ist das eben nicht der Fall. Was wir hier machen, ist wir stellen zwei Typ alias namens Celsius und Fahnhalt, die beide auf den Typen Real verweisen. Das heißt, beide werden durch Real Numbers repräsentiert, stellen aber eigentlich verschiedene Konzepte dar. Also eine Temperatur in Celsius ausgedrückt ist eben nicht dasselbe, wie eine Temperatur in Fahnhalt ausgedrückt und wir wollen eigentlich keine Berechnung sehen, die diese beiden Typen miteinander vermischen. Das heißt, wir wollen nicht, dass Celsius und Fahnhalt als Equivalent betrachtet werden. Wenn wir jetzt aber beide auf Real abbilden und den selben Ansatz, wie hier oben wählen würden, dann würden wir dann doch sagen, dass die beiden miteinander Equivalent sind und die Frage ist jetzt, wie sollte das in so einer Sprache am besten geklärt sein. Die Antwort ist, dass es zwei verschiedene Arten gibt, diese Name Equivalents zu definieren, nämlich Strict Name Equivalents und Loose Name Equivalents. In Strict Name Equivalents ist jeder alias, jeder Typ alias, den ich erstelle, ein neuer Typ, der anders ist als die existierenden Typen. Das heißt, wenn ich sowas hinschreibe wie Type A equal B, dann ist das eine Definition, die einen neuen Typ namens A definiert und der ist dann eben nicht Equivalent zu dem alten Typen B. In Loose Name Equivalents ist das Ganze anders, denn hier erstellen Typ aliase Equivalentetypen. Das heißt, wenn ich sowas hinschreibe wie Type A equals B, dann ist das nur eine Deklaration. Ich definiere keinen neuen Typ, sondern deklariere einfach nur, dass der Typ A ein an. Typ ist der Equivalent ist mit dem Typ B. Und je nachdem, wie die Sprache Name Equivalents jetzt implementiert, ist das manchmal vielleicht das, was man möchte und manchmal nicht das, was man möchte, weil wie wir ja gerade auf der vorigen Folie gesehen haben, gibt es eben Beispiele, wo man eigentlich Strict Name Equivalents möchte und andere Beispiele, wo man eigentlich Loose Name Equivalents haben möchte. Wichtig ist hier, wie immer in Programmiersprachen, man muss wissen, was die Sprache, die man aktuell verwenden, tatsächlich implementiert, sodass man dann keine Überraschung hat, wenn man zum Beispiel solche Typ aliase verwendet. Eine interessante Frage, die relativ verwandt mit der Frage der Typ Equivalents ist, ist, inwiefern man in der Sprache Werte von einem Typ in Werte des anderen Types konvertieren kann. Wenn der Programmierer explizit sagt, dass so eine Konvertierung stattfinden soll, also der Regel mit Hilfe eines Casts, dann spricht man von Typ Konvertierung. Das ähnliche Konzept, wo der Programmierer aber nicht explizit angibt, dass der Typ konvertiert werden soll, nennt sich Type Coercion, und das ist das, was wir ganz am Anfang des Moduls am Javas gibt, bei Beispiel gesehen haben, wo implizit Typ Konvertierung stattfinden. Aber hier geht es jetzt um explizite Konvertierung, also wo der Programmierer explizit diese Konvertierung vornimmt. Da gibt es im Prinzip drei grobe Fälle zu unterscheiden, die wir gleich auch noch mit Beispielen anschauen werden. Der erste Fall ist der, wo die Typen strukturell Equivalents sind. Das heißt, die Typen bedeuten das Gleiche und sind auch gleich am Speicher repräsentiert. Und in dem Fall ist die Konvertierung eigentlich nur konzeptionell. Das heißt, wenn ich den Wert vom einen Typ in den anderen Typ konvertiere, dann passiert er eigentlich zur Laufzeit gar nichts und da wird kein spezieller Code vom Compiler dafür generiert. Der zweite Fall ist der, wo wir Typen haben, die verschiedene Arten von Werte darstellen, die aber zufällig im Speicher gleich repräsentiert sind. Und was hier passieren muss, ist, dass wenn diese Konvertierung stattfindet, überprüft wird, ob der Wert vom einen Typ tatsächlich einfach als ein Wert von dem anderen Typ angesehen werden kann. Was manchmal vielleicht der Fall ist, aber manchmal eben auch nicht. Also hier ist quasi eine Überprüfung notwendig. Der dritte Fall ist, dass die beiden Typen eine andere Repräsentation im Speicher haben. Und hier muss man, um diese Konvertierung durchführen zu können, dann Instruktion ausführen, die tatsächlich den Speicher entsprechend modifizieren und vielleicht die Reihenfolge von Feldern ändern oder vielleicht auch in irgendeiner Form die Repräsentation des Wertes so anpassen, dass er anschließend als Wert des Zieltypes betrachtet werden kann. Schauen wir uns mal ein paar Beispiele dazu an. Und zwar sind die Beispiele zur Abwechslung mal in A da. Und zwar haben wir für das Beispiel 4 Variablen, deren Typ wir deklarieren. Also wir haben die Variable N und sagen N ist ein Integer. Wir haben diese Variable R und sagen, das ist ein sogenannte Long Float. Also eine Art Floating Point Typ. Dann haben wir eine Variable namens T und sagen, die ist vom Typ TestScore. Und wir haben irgendwo anders definiert, dass TestScore ein Integer ist. Und zwar ein Integer zwischen 0 und 100. Dieses Minus Minus hier vorne ist einfach nur die A da Notation für Kommentar. Und dann haben wir noch eine Variable namens C, die den Typ Celsius Temp hat. Also eine Temperatur, die in Celsius ausgedrückt ist. Und das ist irgendwo definiert als ein Typ Allias für Integer. So, mithilfe dieser vier Variablen können wir jetzt eine ganze Reihe von Typ Konvertierung mal anschauen. Und jeweils überlegen, was dann da zur Laufzeit passieren muss, um diese Konvertierung tatsächlich umzusetzen. Jetzt fangen wir mal an mit dem Beispiel, wo wir den Wert aus N nehmen. N ist ja ein Integer. Und den explizit umwandeln, also konvertieren, in eine TestScore und das Ganze dann nach T schreiben. Und dieses Konvertieren oder dieser Cast passiert hier in A da, in der Form, dass wir so eine Art Funktionsaufruf machen. Und der im Prinzip sagt, nimm den Wert N, wandel den um in TestScore und schreib das Ganze dann nach T. Wenn wir das Ganze hier machen, muss zur Laufzeit eine Überprüfung vorgenommen werden. Ich schaue, ob dieser Integerwert tatsächlich in dem Range 0 bis 100 liegt, in dem TestScores ja drin sein müssen. Das heißt, hier findet zur Laufzeit ein Semantischer Check, eine Semantische Überprüfung, statt um eben zu schauen, ob der Wert auch wirklich in dem Range ist, in dem TestScores drin sein sollen. Ein anderes Beispiel ist, dass wir das Ganze jetzt rückgängig machen. Das heißt, wir nehmen jetzt unseren Wert, der in T steht und wollen den wieder nach N schreiben. Und um das machen zu können, müssen wir den Typ explizit konvertieren. Ansonsten würden wir einfach nur vom Compiler hören, dass T und N verschiedene Typen haben. Aber wenn wir das so machen, dass wir den Wert in T nehmen, den nach Integer wieder umwandeln, dann ist das erstmal Typ korrekt. Und jetzt die Frage, was passiert zur Laufzeit? Und hier muss nichts gemacht werden. Und zwar aus dem einfachen Grund, dass dadurch, dass TestScore ja als Integer in einem bestimmten Range definiert ist und aber Integer natürlich alle möglichen Integer-Werte haben können, ist jeder TestScore automatisch ein Integer und da geht es keine Überprüfung nötig, sondern das kann einfach konvertiert werden. Schauen wir mal einen dritten Fall an. Und zwar nehmen wir jetzt unseren Integer-Wert, der in N ist und schreiben den jetzt nach R, indem wir ihn umwandeln in so einen Long Float. Und was hier passiert ist, dass wir quasi einen Integer-Wert nehmen und den aber konvertieren in einen Floating-Point-Wert. Und hier ist eine Runtime Conversion von Nöten. Also hier wird zur Laufzeit der Typ tatsächlich umgewandelt, ganz einfach, weil Integers und Floating-Point-Zahlen im Speicher ganz anders repräsentiert werden. Das heißt, hier werden tatsächlich die Bits, die im Speicher sind, angepasst, sodass simantisch zwar noch dieselbe Zahl drin steckt, das Ganze aber im Speicher ganz anders repräsentiert wird. Wenn ich das Ganze jetzt wieder rückgängig mache, also wenn ich unseren Wert, der jetzt in R steht, nehme und den anschließend wieder in unsere Integer-Variable N schreiben will, dann kann ich das so machen. Und was jetzt hier passiert, ist, dass A auch wieder eine Umwandlung nötig ist. Und wir B außerdem aber auch noch eine Überprüfung brauchen. Die schaut, ob der Floating-Point-Wert überhaupt verlustfrei in ein Integer umgewandelt werden kann, weil wenn der Floating-Point-Wert zum Beispiel 2,5, es kann ich den ja nicht verlustfrei in ein Integer umwandeln, weil ich dann ja etwas abschneiden oder runden müsste. Und dann schauen wir nochmal Beispiele an, die C und N miteinander verknüpfen. Und zwar einmal das Beispiel, dass wir ein Wert, der NC gespeichert ist, nehmen und den aber in ein Integer umwandeln, um ihn dann nach N zu schreiben. Und weil C in unserem Fall oder beziehungsweise weil Celsius-Temp einfach nur ein Typ alias für Integer ist, wäre das eine rein konzeptionelle Geschichte. Das heißt, das muss man zwar hinschreiben, um das Programm statisch typkurekt zu machen, aber zur Laufzeit würde hier gar nichts passieren, sondern der Wert bleibt einfach, wie er ist im Speicher. Und genauso, wenn wir das Ganze andersrum machen, also wenn wir unseren Wert in N nehmen und in einer Celsius-Temperatur umwandeln, das Ganze dann in C reinschreiben. Hier oben wollte ich übrigens C schreiben. Dann ist auch das wieder rein konzeptionell, weil diese beiden Typen ja alias sind und zumindest in A damit komplett kompatibel sind. Ja, und damit sind wir auch schon am Ende von Teil 4 von 6 hier im Modul Typ-Systeme. Sie wissen jetzt also ein bisschen mehr darüber, was Typen denn eigentlich equivalent macht, bzw. wie Typ-Equivalenz in Sprachen definiert sein kann. Und Sie haben gesehen, dass das in verschiedenen Sprachen ganz verschieden gemacht werden kann. Und Sie haben auch gesehen, wie sich das auf die Konvertierung von Werten von einem Typ in einen anderen Typ auswirkt. Damit vielen Dank fürs Suche und bis zum nächsten Mal.