 Ja, herzlich willkommen zurück zu Programmierparadigmen, in diesem dritten Teil im Modul Typ Systeme soll es um Polymorphismus gehen und was das genau ist, werden wir gleich sehen. Bevor wir uns anschauen, was Polymorphismus bedeutet, sollt mir vielleicht erstmal noch ein bisschen genauer klären, was Typ eigentlich genau bedeutet. Also so die praktische Art auf Typen zu schauen, haben wir in den ersten zwei Teilen das Modul schon gesehen. Jetzt denken wir da nochmal ein bisschen genauer drüber nach und da gibt es im Prinzip drei Interpretationen, was ein Typ eigentlich ist. Zum einen kann man sagen, ein Typ ist im Prinzip einfach eine Menge von Werten und in dieser denotationalen Art und Weise zu definieren, was ein Typ ist, beschreibt diese Menge von Werten eigentlich, was der Typ ist. Der Typ ist gleich eine Menge von Werten. Die zweite Variante ist, dass wir den Typen strukturell beschreiben. Das heißt, wir sagen, ein Typ ist entweder ein eingebauter primitiver Typ, so was wie ein Int oder ein zusammengesetzter Typ, der aus diesen primitiven Typen zusammengebaut wird, zum Beispiel so was wie ein Record in C. Und alles, was diese Struktur hat, die von diesem Typen beschrieben wird, also zum Beispiel jeder Wert, der vielleicht aus zwei Feldern besteht, nämlich ein Int und dann ein Bullen, ist vom selben Typ. Das ist auch eine Art und Weise, wie man Typen definieren kann. Die dritte ist, dass man sagt, ein Typ ist eigentlich eine Abstraktion, der so eine Art Interface, also eine Schnittstelle anbietet, um eine Menge von Operationen, die man auf Werten dieses Types ausführen kann, zu beschreiben. Und das ist nochmal eine ganz andere Sicht, weil hier geht es nicht darum, welche Werte das genau sind oder welche Struktur diese Werte haben, wenn man sie im Speicher anschaut, sondern es geht eigentlich darum, welche Operationen man auf diesen Werten dann ausführen kann. In der Praxis ist in den meisten Sprachen eigentlich eine Kombination von allen drei Definitionen, was so ein Typ eigentlich ist, das was verwendet wird. Das heißt, ein Typ beschreibt natürlich irgendwie schon eine Menge von Werten. Es geht oft auch um strukturelle Eigenschaften dieser Typen, zum Beispiel, dass bestimmte Felder vorhanden sind. Und natürlich werden Typen in der Praxis auch häufig verwendet, um eine bestimmte Schnittstelle oder eine Interface zu beschreiben, das heißt, um zu sagen, welche Operation man darauf ausführt. So, was ist jetzt eigentlich dieser Polymorphismus? Polymorphismus, der Begriff selbst, kommt aus dem Griechischen und bedeutet zu viel wie verschiedene Formen haben. Im Zusammenhang mit Programmiersprachen und Typen in Programmiersprachen bedeutet das in der Regel, dass es eine Variable oder vielleicht auch ein Stück Code gibt, der eben nicht, oder das eben nicht nur ein Typ hat, sondern mehrere Typen haben kann, die sonst vielleicht auch gar nicht miteinander kompatibel sind. Aber wo eben dieses Stück Code oder diese Variable oder diese Werte im Speicher trotzdem aus verschiedenen Sichten betrachtet werden kann, nämlich mit verschiedenen Typen. Das dritte in der Regel in zwei Arten und Weisen auf und das erste davon ist der sogenannte Parametric Polymorphism. Also hier ist die Idee, dass ich ein Stück Code habe und dieses Stück Code parametrisiert wird mit Hilfe eines Typen oder vielleicht auch mit mehreren Typen, die dann beschreiben, was der Code eigentlich bedeutet. Das heißt aber, ich schreibe den Code nicht einmal hin und sage, dieser Code gilt für diesen Typ, sondern ich schreibe den Code einmal hin und sage, wenn du diesen Code benutzt, musst du ihn noch instantiieren, indem du diesen ein Parameter oder mehrere Parameter angibst, die ein Typ sind. Und dann hat der Code erst eine Bedeutung. Ein Beispiel dafür sind die sogenannten Generics oder auch die Container in Java beziehungsweise C++, wo ich zum Beispiel Implementierung von Datenstrukturen aufschreiben kann wie eine Liste, ohne zu sagen, welche Arten von Werten in dieser Liste gespeichert sind, trotzdem aber angeben kann, dass die Werte, die in dieser Liste gespeichert sind, alle denselben Typen haben müssen. Das heißt, ich habe einen Typ Parameter, der angibt, welche Art von Wert ich in dieser Liste speichere und um dieses Stück Code, diese Implementierung der Liste jetzt zu verwenden, muss ich diesen Typ Parameter mit einem konkreten Typen instantiieren und dann zum Beispiel sagen, ich möchte eine Liste von Strings haben. Die zweite Art und Weise, wo Polymorphismus in Programmiersprachen auftritt, ist Subtype Polymorphism. Hier ist die Idee, dass ich eine Typ Hierarchie habe, also zum Beispiel die Klassen, die wir in Java oder C++ haben, und ich kann eine Superklasse oder ein Supertyp verfeinern, indem ich bestimmte Änderungen dann in der Subklasse vornehme und dadurch ein Subtypen bekomme. Und der Polymorphismus tritt dann in der Form zu Tage, dass ich dann zum Beispiel eine Variable vom Typen der Superklasse haben kann, die aber dann zur Laufzeit bestimmte Werte haben kann, nämlich all die Werte, die auch in den Subtypen geschrieben sind. Und damit hat diese Variable eben nicht nur ein Typ, sondern eigentlich mehrere, weil all die Subtypen damit auch gemeint sind. Schauen wir uns das Ganze am besten mal wieder mithilfe eines Beispiels an, und zwar fangen wir an mit einem Beispiel für diesen Parametric Polymorphism, und zwar in Java. Also was ich hier habe, ist einfach eine Klasse, die soll ein binary tree, also einen binären Baum repräsentieren, und zwar habe ich in diesem binären Baum Knoten, jede Instanz dieser Klasse repräsentiert einen solchen Knoten. Und an jedem Knoten speichere ich ein Wert eines bestimmten Types, nämlich vom Typ T. Und jetzt möchte ich aber nicht angeben, jetzt möchte ich diesen Typen T hier nicht festlegen und sagen, das muss immer zum Beispiel ein int sein oder so, sondern ich lasse das als Typparameter. Und das mache ich hier oben mit dieser Typparameter Notation von Java, wo ich sage, diese Klasse hat diesen Typparameter, wann immer ich diese Klasse verwenden möchte, muss ich diesen Typparameter dann konkretisieren. Aber in der Implementierung der Klasse muss ich das erstmal nicht konkretisieren, sondern ich rede einfach über diesen Typen T, ohne zu wissen, was für ein Typ das eigentlich genau ist. Und dann neben dem eigentlichen Wert, der hier in diesem Knoten des binären Baums gespeichert ist, habe ich noch zwei weitere Felder, nämlich für die Kinder des Knotens, also das left child und right child. Und was ich möchte, ist, dass in meinem Binärbaum alle Knoten Werte des selben Types haben. Das heißt, ich möchte, dass der binary tree, der auf der linken Seite dann kommt, auch wieder ein binary tree vom Typ T ist. Das heißt, ich benutze hier dasselbe T und das gleiche dann auch für das Kind auf der rechten Seite. Dann habe ich hier noch diese eine Methode, praktisch hätte man natürlich noch ein paar mehr Methoden, aber diese eine ist einfach nur um den Wert des aktuellen Knotens zu setzen, wo ich sage, ich nehme ein Wert, der eben auch wieder diesen Typ T haben muss und schreibe den dann in das entsprechende Feld namens value. So, jetzt benutzen wir die Klasse mal hier unten und instanziieren die und wenn ich das mache, muss ich jetzt natürlich diesen Typen angeben und in dem Fall sage ich, ich möchte ein binärbaum haben, indem die Knoten string Werte speichern und nenne das Ganze dann string tree. Und wenn ich den jetzt verwende und da zum Beispiel set value aufruf und hier jetzt eine Zahl ein integer reingebt, dann sollte ich vom compiler ein Fehler bekommen. Kompilieren wir das mal aus Spaß, der mir dann hoffentlich sagt, genau, der mir erst mal sagt, dass ich den Rest schreibfehler habe. Okay, mach mal den weg und dann sagt es mir auch die IDI schon, aber ich frag trotzdem nochmal den compiler, der sagt mir dann, dass ich an dieser Stelle ein int übergebe, aber das eben nicht kompatibel ist mit dem Typ string, der ja hier erwartet ist, weil ich diese Klasse bin tree eben mit string instanziiert habe. Das heißt, ich muss das jetzt ändern, zum Beispiel in diesen string abc und dann kann ich das machen und diese Typ Sicherheit, die ich mit Hilfe dieser parametrischen Typen bekomme, geht dann natürlich auch noch weiter. Also wenn ich jetzt zum Beispiel aus dem string tree den Wert auslese und den jetzt wieder in sagen wir mal eine bultche variabtisch schreiben wollte, dann würde ich da hoffentlich auch wieder ein Fehler für kriegen. Genau, der mir sagt, dass ich hier zwei Variable vom Typ bool habe, der Wert, den ich von string tree dort value zurück kriege, aber eben vom Typ T, also in dem Fall vom Typ string sein wird und das Ganze deswegen nicht funktioniert. Und wenn ich jetzt aber da eine Variable vom Typ string draus mache, dann sollte das Ganze hoffentlich kompilieren. Genau, wunderbar, jetzt ist das Programm also Typ korrekt. Also was uns dieser Parametric Polymorphism gibt, ist, dass ich diesen Code dieser Klasse schreiben kann, ohne den Typen T zu kennen. Ich lasse den einfach als Typparameter. Schlussendlich, wenn ich diese Klasse dann aber verwende, eben Typ Sicherheit dadurch bekomme, dass dieser Typ T ein bestimmter Typ sein muss und die entsprechenden Checks dann auch mit diesen konkreten Typen durchgeführt werden. Dann schauen wir uns mal noch ein Beispiel für die zweite Form von Polymorphismus an, nämlich Subtype Polymorphism und hier ist die Sprache wieder Java. Was wir hier haben, sind drei einfache Klassen, nämlich eine Klasse A und dann zwei Subklassen dieser Klasse A namens B und C. Dann habe ich dann eine Methode, die ein Objekt vom Typ A erwartet und damit irgendwas macht. Wichtig ist einfach nur, dass der deklarierte Typ des Arguments, den diese Methode erwartet, A ist. Und hier unten benutze ich das Ganze dann jetzt, indem ich diese UseA Methode mal aufrufe. Und eine Sache, die ich hier machen kann, ist, dass ich ein Objekt vom Typ A übergebe, zum Beispiel dieses neu erstellter A, was ich hier insenziere. Und wenn ich das jetzt mache, was ist das Problem? Sollte das eigentlich korrekt sein, auch wenn der Editor da gerade nicht übereinstimmt. Und wenn ich das jetzt kompiliere, dann funktioniert das Ganze auch. Und das Interessante ist, dass ich hier natürlich nicht nur einen A übergeben kann, sondern eben auch Instanzen aller Unterklassen, zum Beispiel ein Objekt vom Typ B. Und auch das ist okay, also auch wenn ich das kompiliere, funktioniert das ganz einfach aus dem Grund, weil B eine Subklasse von A ist. Und ich diesen Code, der das A hier benutzt, in dem Fall nicht wirklich, aber der das A hier benutzen könnte, so geschrieben haben muss, dass der auch mit Bs und Cs umgehen sollte. Denn das sind ja Subtypen von A und damit eben auch mögliche Argumente, die ich in diese Methode hier übergeben kann. Also diese Art von Polymorphismus gibt es auch und die bedeutet im Prinzip einfach nur, dass diese Variable hier verschiedene Typen haben kann, nämlich nicht nur A, sondern auch alle Untertypen von A. In manchen Programmiersprachen gibt es noch eine dritte Form von Polymorphismus, nämlich Polymorphe-Variablen. Was das bedeutet, ist, dass eine Variable Objekte von verschiedenen Typen speichern kann. Also in vielen, vor allem statisch typisierten Sprachen ist es ja so, dass ich die Variable definiere und in dem Moment wo ich die, oder deklariere, in dem Moment wo ich die deklariere, festlege, welchen Typ diese Variable hat. Und dann können eben nur Werte, die diesen Typen haben, in der Variable gespeichert werden. In Sprachen, die Polymorphe-Variablen erlauben, ist das nicht so, sondern ich kann in derselben Variable ganz verschiedene Typen speichern. Das heißt zu verschiedenen Zeitpunkten während der Programmausführung sind eben verschiedene Arten von Werten in dieser Variable drin. Hier unten ist mal ein Beispiel aus irgendeiner Pseudosprache, könnte zum Beispiel JavaScript oder Python sein, wo ich diese Variable A habe. Zunächst erstmal ein String reinschreibe, anschließend, ich habe noch eine zweite Variable namens B, die hat ein Int oder eine Zahl, schreibe das dann hier in A rein. Das heißt in dem Moment hier hat A einen anderen Typen als was es hier oben hatte, nämlich plötzlich ist es kein String mehr, sondern ein Int. Und schliessendlich schreibe ich dann hier unten wie den anderen String rein, das heißt A hat jetzt wieder die Variable String, den Typen String. Und wir haben sozusagen während der Laufzeit des Programms den Typ von A einfach mal gewechselt. Interessanterweise ist das in den meisten dynamisch typisierten Sprachen typ korrekt, auch in manchen staatisch typisierten Sprachen, aber vor allem in dynamisch typisierten Sprachen. Und man kann das so aufschreiben. Ob das jetzt eine gute Idee ist, sei mal da hingestellt, in den meisten Fällen würde ich eher dazu raten, jeder Variable nur einen Typen tatsächlich zu geben, weil das ganz einfach einfacher zu verstehen ist, aber es gibt tatsächlich auch Situationen, wo es durchaus Sinn macht, verschiedene Typen in derselben Variable zu speichern. Und dann ist das ein Weg, wie man das tatsächlich machen kann. Jetzt haben wir viel über Polymorphismus geredet. Jetzt möchte ich noch kurz auf drei spezielle Typen und Werte hinweisen, die es in vielen Sprachen in der einen oder anderen Form gibt und die mit dem Typsystem doch in interessanter Art und Weise oft interagieren. Das eine ist der Void-Type, der heißt in vielen Sprachen Void, manchmal ist da auch ein bisschen anders, aber die Grundidee ist, dass es ein spezieller Typ ist, der eigentlich sagt, dass es hier gar keinen Typ gibt und dass es dann auch gar nicht eine Menge von Werten gibt, sondern eigentlich nur einen trivialen Wert, nämlich der Wert, der gar nicht da ist. Also diesen Void-Typen kann ich zum Beispiel verwenden, um in einer Sprache anzugeben, dass eine Funktion eben kein Rückgabewert hat. Dann sage ich, die hat den Void-Type als Rückgabetyp und gebe damit an, dass im Prinzip kein Wert zurückgegeben wird. Ein zweites Beispiel von so einem Spezialfall ist Null, also das ist einfach ein spezieller Wert, der sagt, hier ist gerade eben kein Wert von dem bestimmten Typen, den die Variable vielleicht sonst hat, sondern eben Null, was eigentlich kein richtiger Wert ist. Also wenn ich zum Beispiel ein Objekt normalerweise in eine Variable eines bestimmten Types speichern würde, zum Beispiel in Java, kann ich stattdessen auch Null reinschreiben und dieser Null-Wert bedeutet einfach, dass in der Variable in dem Moment eben kein Wert von dem Typ ist, sondern dieser Spezialwert Null. Und dann gibt es in vielen Sprachen noch sogenannte Option Types, die einfach angeben, dass eine Variable optional ein Wert eines bestimmten Types haben kann, aber das nicht so sein muss, sondern ich stattdessen auch sowas ähnliches wie den diesen Null-Value drin haben kann. In Python gibt es das zum Beispiel, wo ich Variable annotieren kann mit sowas wie Option of Int und das bedeutet dann ganz einfach, dass diese Variable entweder tatsächlich ein Int enthält oder eben Null und Null ist im Prinzip so dasselbe wie dieser Null-Value, den man vielleicht aus Java kennt, also ein Wert, der angibt, dass aktuell in der Variable eben nichts drin steht. So, zum Abschluss dieses Teils des Modules Types-Systeme habe ich wieder ein kleines Quiz und zwar in der Form von vier Sätzen, von denen manche richtig sind und manche falsch sind und ich würde Sie bitten, diese Sätze einfach mal durchzulesen, zu überlegen, was davon ist richtig oder falsch ist, das Video dann anzuhalten und in Ilias abzustimmen, so dass Sie sehen, ob Sie dann auch tatsächlich richtig lagen und anschließend verrate ich dann die Lösung. So, schauen wir uns mal die Lösung an, also von diesen vier Sätzen waren drei falsch, der erste ist falsch, weil er sagt, dass ein Typ-System überprüft, ob alle Typen in einem Programm equivalent sind, aber das ist nicht das, was das Typ-System überprüft, sondern das überprüft, ob die Operationen, die auf Werten angewandt werden, kompatibel sind mit den Typen, die die Werte haben, aber das heißt nicht, dass alle Typen in dem Programm equivalent sein müssen. Der zweite Satz ist auch falsch, das ist ein bisschen komplexer, weil das Vereint quasi wissen aus diesen Modulen mit dem, was wir in dem früheren Modul schon gesehen haben zum Thema Scoping. Hier war die Aussage, dass Programmiersprachen mit Dynamic Scoping statisch typisiert sein können, das ist aber nicht so, denn in einer Sprache mit Dynamic Scoping weiß ich ja erst zur Laufzeit, auf welche Werte bestimmte Namen denn überhaupt verweisen und wenn ich noch nicht mal weiß, auf was für Werte diese Namen eigentlich verweisen, dann kann ich statisch auch nicht überprüfen, ob diese Werte denn die richtigen Typen haben, das heißt ich kann so eine Sprache mit Dynamic Scoping eben nicht statisch typisieren oder statisch die Typen überprüfen. Der dritte Satz stimmt zur Abwechslung mal, also subclasses sind tatsächlich eine Form von Polymorphic Typing, haben wir ja gerade auch schon den Beispiel zugesehen und der letzte Satz stimmt wieder nicht, also hier wurde behauptet, dass Option Types, also diese Typen, die angeben, dass entweder was da ist oder eben nichts da ist, nur in Strongly Typed Languages nicht existieren können, aber das stimmt natürlich nicht, denn wie gesagt fast alle Sprachen können zeigen fast alle Sprachen, die wir heutzutage so haben sind Strongly Typed und diese Option Types existieren natürlich in diesen Sprachen, das heißt der Satz ist falsch und interessanterweise gibt es diese Option Types eben nicht nur in Strongly Typed Languages, sondern auch in Statically Typed Languages zum Beispiel in Scala oder auch anderen Sprachen, wo es eben speziell ein Typ gibt, der sagt, dass ein Wert entweder von diesem bestimmten Typ ist oder eben kein Wert enthält, beziehungsweise diesen Spezialtypen. Ja, und damit sind wir auch am Ende dieses dritten Teils des Moduls Typ Systeme, ich hoffe, Sie wissen jetzt ein bisschen mehr darüber, was Polymorphismus in Programmi sprachen eigentlich bedeutet und ja, in den verbleibenden drei Teilen des Moduls schauen wir uns dann noch weitere Aspekte von Typ Systeme an. Damit erstmal Danke fürs Zuhören und bis zum nächsten Mal.