 Herzlich willkommen zurück zur Veranstaltung Programmierparadigmen. Wir sind hier immer noch im Modul Typ-Systeme und in diesem zweiten Teil des Moduls soll es darum gehen, wie Typ-Systeme und Typen überhaupt in Programmiersprachen integriert sein können. Also wir werden so verschiedene Arten wie Typen in Programmiersprachen auftreten und anschauen. Es gibt ja so eine ganze Reihe von Begriffen, die beschreiben, inwiefern eine Programmiersprache denn Typen unterstützt. Und einer dieser Begriffe ist Strongly Typed. Was heißt das eigentlich, wenn eine Programmiersprache Strongly Typed ist? Also die Grundidee ist einfach, dass jede Operation nur auf bestimmten Typen angewandt werden kann und dass die Programmiersprachen Implementierung sicherstellt, dass diese Operation eben nur auf Werten, die den richtigen Typen haben angewandt werden können. Wenn Ihnen das jetzt so vorkommt, als müsse das ja so sein, dann ist das wahrscheinlich, weil quasi jede Programmiersprache, die seit den 70ern entwickelt wurde, Strongly Typed ist. Also fast jede Sprache, die sie vermutlich mal irgendwo verwendet haben, hat diese Eigenschaft. Eine Sprache, die da so ein bisschen rausfällt oder zumindest teilweise rausfällt, ist C, denn C ist meistens Strongly Typed, aber es gibt doch ein paar Ausnahmen in dieser Sprache. Eine Ausnahme ist zum Beispiel, dass man Subroutine oder Funktion in C mit einer variablen Anzahl von Parametern aufrufen kann. Das heißt, es ist eigentlich nicht definiert, wie viel Parameter diese Funktion tatsächlich bekommen muss, sondern man kann die dann mit verschiedenen Anzahlen von Parametern tatsächlich aufrufen. Eine andere Ausnahme ist, dass man in C Pointer und Arrays eigentlich gleich verwenden kann. Also ich kann jedes Array als ein Pointer auf ein Stück Speicher betrachten und diesen Pointer dann vielleicht auch weiter rücken und arithmetisch bearbeiten. Oder umgekehrt kann ich jeden Pointer auch wieder als Array anschauen und der Speicher, auf den so ein Pointer zeigt, einfach als Array betrachten. Also an der Stelle ist quasi nicht definiert, dass eine bestimmte Operation nur auf eine bestimmte Art von Wert oder auf Werte eines bestimmten Types angewandt werden kann. Aber in den meisten Fällen ist das in C eigentlich auch der Fall und wie gesagt in neueren Sprachen ist das eigentlich generell der Fall. Also quasi fast alle Sprachen, die ihn so über den Weg laufen, sind Strongly Typed. Ein anderer Begriff, der so ein bisschen ähnlich klingt und manchmal deswegen auch verwechselt wird, aber eben nicht dasselbe bedeutet, ist Statically Typed. Statically Typed bedeutet, dass die Programmiersprache A erstmal Strongly Typed ist und B, das ist jetzt der wichtige Teil, dass diese Eigenschaft zur Compile Zeit, also wenn das Programm kompiliert wird, bevor es tatsächlich ausgeführt wird, überprüft wird. Das heißt der Compiler oder besser gesagt der Typechecker, der vielleicht Teil des Compilers ist, überprüft beim Kompilieren, ob dieses Programm tatsächlich wohl typisiert ist, also ob die Ausdrücke und Operation tatsächlich alle den entsprechenden Typ haben, der vielleicht anotiert ist und ob diese Typen auch zusammenpassen, wenn mehrere Werte in Operation verbunden werden. Wenn man es ganz genau nimmt, ist eigentlich fast keine Programmiersprache, die es praktisch gibt, wirklich komplett Statically Typed, weil es eigentlich doch irgendwie immer Ausnahmen gibt. Eine Sprache, die hauptsächlich Statically Typed ist zum Beispiel ist Java. Allerdings gibt es in Java dann eben doch ein paar Sprachfeatures, die da eben eine Ausnahme darstellen. Also ein Beispiel sind zum Beispiel Abcasts, also wo ich ein Wertcast in einen spezifischeren Typ oder auch Reflection, wo ich eben zur Laufzeit zum Beispiel anschauen kann, welche Methoden hat denn eigentlich mal ein Objekt und da vielleicht eine Methode aufruft, die mit dem normalen Typ des Objektes gar nicht sichtbar will, aber mithilfe von Reflection kann ich das dann eben doch machen. Das heißt, wenn man in der Praxis von Statically Typed Programmiersprachen redet, meint man damit eigentlich, dass das hauptsächlich Statically Typed ist. Es gibt aber immer so ein paar Ausnahmen, die man, wenn man diesen Begriff verwendet, dann aber eigentlich ignoriert. Als kleines Beispiel für eine Sprache, die Statically Typed ist und wo ich gerade mal diese Ausnahme, diese Eigenschaft demonstrieren möchte, schauen wir uns hier mal ein bisschen Java Code an. Also hier habe ich drei Klassen, eine Klasse A und dann zwei Klassen, die diese Klasse erweitern, also sozusagen Kinder von A sind, nämlich B und C. Und dann habe ich hier unten in der Main-Methode des kleinen Programmes folgendes. Ich erstelle erstmal eine Liste von Dingen und sage, das ist eine Liste von As. Das ist dann eine Array-List und dann füge ich in diese Liste ein Objekt ein, nämlich ein Objekt, was ich mit newB erstelle, also was ein Objekt von Typ B sein wird. Da B ein Subtyp von As ist, kann ich das machen, wenn ich sage, es ist eine Liste von As, dann kann dann natürlich auch ein B oder zum Beispiel auch ein C drin sein. Und was ich dann hier mache, ist, dass ich aus dieser Liste das erste Element wieder rausnehme. Wie wir wissen, wird uns das dieses Objekt von Typ B zurückgeben. Was ich hier aber mache, ist, ich caste dieses zurückgegebene Objekt zum Typ C und speichere es dann in diese Variable, die auch den Typ C hat. Jetzt sieht man den Code und wenn man ein bisschen darüber nachdenkt, wird man sehen, das kann eigentlich nicht sein, weil ich stecke ein B rein, dann kann ich kein C rausbekommen. Und das Interessante ist, obwohl Java statically typed ist, wenn ich das Ganze jetzt kompiliere, dann bekomme ich kein Fehler. Das heißt, statisch ist das ganze Typ korrekt und obwohl die Sprache statically typed ist, wird an der Stelle kein Fehler ausgegeben. Wenn ich das Ganze jetzt aber ausführe, dann bekomme ich zur Laufzeit tatsächlich ein Fehler, nämlich diese Class Cast Exception. Wie mir sagt, dass ich eben dieses Objekt vom Typ B nicht in eine Variable vom Typ C speichern kann. Das heißt, dieses Programm ist tatsächlich zur Laufzeit doch nicht typkurekt und weil die Sprache aber eben nur hauptsächlich statically typed ist, erkenne ich das Ganze eben leider erst zur Laufzeit. Das Gegenstück zu Sprachen, die statically typed sind, sind Sprachen, die dynamically typed sind, also wo das Type Checking erst zur Laufzeit des Programmes ausgeführt wird. Das heißt, es gibt eigentlich keinen Compiler, der oder zumindest kein Typsystem in einem Compiler das überprüft, ob das Programm Typkurekt ist, sondern erst zur Laufzeit, wenn das Programm dann tatsächlich ausgeführt wird, wird überprüft, ob all die Operationen, die stattfinden, tatsächlich typkurekt sind. Das bedeutet in der Praxis, dass bestimmte Typfehler erst relativ spät im Laufe der Entwicklung gefunden werden können, nämlich erst dann, wenn der Code tatsächlich ausgeführt wird. Es kann sein, dass das während des Testens des Codes schon passiert, es kann aber auch sein, dass das tatsächlich erst passiert, wenn der Code dann wirklich in der Produktion irgendwo läuft. Eine Klasse von Sprachen, wo diese dynamische Typisierung besonders häufig auftritt, sind die sogenannten Scripting-Sprachen. Ich habe die hier mal in Anführungszeichen gepackt, weil die Sprachen mittlerweile häufig für Dinge verwendet werden, die einfach über das, was man sich so unter Script vorstellt, hinausgehen. Aber die heißen trotzdem irgendwie noch so, also ich meine damit Sprachen wie zum Beispiel JavaScript oder Python. Was wichtig ist, zu verstehen, ist, dass auch in diesen dynamisch typisierten Programmiersprachen jeder Wert einen Typ hat. Das heißt, es ist nicht so, dass der Wert jetzt keinen Typ hat und zur Laufzeit irgendwas passiert, sondern jeder Wert hat tatsächlich einen Typ. Und wenn es Typfehler gibt, dann werden die auch trotzdem als solche Typfehler ausgegeben, bloß halt in der Regel dann erst als Laufzeitfehler und eben nicht als statische Fehler, die schon während des Kompilierens auftreten. Jetzt haben wir diese zwei Arten gesehen, statically typed, dynamically typed. Es gibt zum Glück was in der Mitte und das nennt sich Gradual typing. Die Idee von Gradual typing ist, dass wir im Prinzip so das Beste aus statisch typisierten und dynamisch typisierten Programmiersprachen nehmen, nämlich A. Dem Programmierer, die Freiheit lassen, schnell mal Code hinzuschreiben, ohne da vielleicht schon alle Typen zu definieren oder zumindest zu anotieren. Und gleichzeitig dann aber, wenn das Programm vielleicht größer wird und wir den Code wattbarer und lesbarer und erweiterbarer machen wollen, dann doch die ganzen Vorteile von statisch anotierten Typen zumindest teilweise zu bekommen. In diesen Sprachen, die Gradual typing unterstützen, ist es optional Typen zu anotieren. Das heißt, ich muss für eine bestimmte Variable zum Beispiel nicht festlegen, welchen Typen die hat. Ich kann es aber machen, wenn ich möchte. Und das hat eben genau diesen Vorteil, dass man schnell Code schreiben kann und die Typen, wenn man möchte, dann später noch hinzufügen kann. Der statische Type Checker, den es dann für solche Sprachen gibt, der nimmt im Prinzip all die Typen anotationen, die da sind, aber ignoriert alles, was nicht da ist. Das heißt, der Type Checker würde Warnungen ausgeben, wenn die Typen, die anotiert sind oder die vielleicht trivial inferiert werden können, nicht miteinander kompatibel sind. Das heißt, wenn das Programm basierend auf den Informationen, die da sind, typinkorrekt ist, dann gibt es tatsächlich einen Fehler. Aber er kann natürlich dadurch nicht alle Typfehler finden, weil eben nicht alles vollständig anotiert ist. Das heißt, so ein Gradual Type Checker gibt eben keine Garantie, dass alle Typfehler gefunden werden. Aber wenn er was findet, dann ist da in der Regel tatsächlich was falsch, weil das dann ein Widerspruch in den anotierten Typen darstellt. Schauen wir uns einfach mal ein praktisches Beispiel an für eine Sprache, die dieses Gradual typing umsetzt, nämlich Python. Also, was wir hier sehen, ist eine Funktion, die heißt addNumbers und wie der Name so ein bisschen suggeriert, soll die eigentlich zwei Zahlen als Argumente nehmen und dann deren Summe zurückgeben. Was wir jetzt hier unten machen ist, wir definieren diese zwei Variablen a und b, a ist auch tatsächlich eine Zahl, nämlich 23, aber b ist ein String. Und dann geben wir diese beiden Werte an, b einfach mal in die addNumbers Funktion hinein. Das ist jetzt Python ohne irgendwelche Typannotation. Das heißt, der Type Checker, den ich da vielleicht darauf anwenden könnte, hat eigentlich keine Chance rauszufinden, dass hier was falsch ist. Und der Fehler wird dann erst offenbar, wenn ich diesen Code ausführe, also wenn ich das jetzt ausführe mit dem ganz normalen Python Interpreter, dann bekomme ich hier unten diesen Fehler, der mir sagt, dass diese beiden operanten m und n eben vom Typ int und string sind und ich das nicht mit einem plus verbinden kann und ich deswegen einen Typfehler habe. Aber ich erkenne diesen Typfehler jetzt eben erst zur Laufzeit, weil Python zumindest erst mal zumindest eine dynamisch typisierte Sprache ist. So was kann ich jetzt machen, weil Python eben auch Gradualtyping unterstützt, kann ich zum Beispiel in diese Funktion jetzt bestimmte Typen anotieren und zum Beispiel sagen, dass das erste Argument und auch das zweite Argument, die ich in diese Funktion reingebe, ein Integer sein müssen und ich kann zum Beispiel auch angeben, dass der Return Value dieser Funktion auch ein Integer sein muss. Und wenn ich den Code jetzt, bevor ich ihn ausführe, zum Beispiel mit mypy, das ist einer dieser GradualType Checkers für Python, mal überprüfen, dann sollte der mir hoffentlich sagen, dass mein Code inkorrekt ist, weil ich eben in dem Aufruf in Zeile 6 kein Integer übergebe, sondern ein string und siehe da. Das passiert auch. Also ihr sagt mir hier das zweite Argument von addNumbers in Zeile 6 ist eben inkompatibel mit dem typen string, den ich deklariert habe und ich müsste, sorry, ist inkompatibel mit dem anotierten Typ Int und ich müsste, weil ich hier eben einen string übergebe und das ist falsch. Das heißt, was ich jetzt hier machen müsste, ist irgendwie einen anderen Wert übergeben, sagen wir mal 5 und eben nicht das b. Und dann sollte der GradualType Checker hoffentlich auch zufrieden sein. Was man im Prinzip damit bekommt, ist, dass man den Code in Python erstmal ganz gut hinschreiben kann, ohne sich vielleicht allzu sehr um die ganzen Typen zu kümmern. Und dann aber irgendwann, wenn die Codebasis vielleicht ein bisschen stabiler wird, diese Typen dann doch noch hinzufügt und dadurch eben die statische Typ-Sicherheit, wie man sie vielleicht aus statisch typisierten Sprachen wie Java kennt, dann eben auch in eigentlich dynamisch typisierten Sprachen wie Python bekommt. So, um zu überprüfen, was jetzt von dem ganzen Hengen geblieben ist, hätte ich mir mal ein kleines Quiz und die Idee ist wie immer, dass Sie dieses Quiz anschauen, das Video anhalten und dann in Illias abstimmen, um zu schauen, ob das, was Sie hier als Antwort so denken, denn tatsächlich richtig ist und dann schauen wir uns die Lösung an. Also, die Frage ist hier für dieses Stück Code, was hier unten gegeben ist, was im Prinzip in jeder Programmiersprache geschrieben sein könnte. Also, das ist jetzt nicht die syntax einer bestimmten Sprache, sondern einfach mal so eine syntax, die hoffentlich allen irgendwie einleuchtet. Und die Frage ist, was passiert denn, wenn ich diesen Code jetzt kompiliere oder dann versuche auszuführen, wenn ich eine strongly typed Sprache habe, eine statically typed Sprache oder eine dynamically typed Sprache. Und ja, die Frage ist, was passiert in diesen drei Fällen? So, schauen wir uns mal die Lösung an. Also, fangen wir mit der ersten Art Sprache an, nämlich einer Sprache, die strongly typed ist. Was da hier passiert ist, ich werde einen Typfehler bekommen. Und der Grund ist ganz einfach, dass ich in dieser Zeile ja A plus A berechne. A ist ein integer oder eine Zahl. Das heißt, C wird auch eine Zahl sein. Und hier versuche ich dann von dieser Zahl den Wert zu subtrahieren, der in B ist. B ist aber true, also ein Boolean. Und das geht zumindest nach den Regeln fast alle Sprachen eben nicht. Und deswegen würde ich in jeder strongly typed Sprache da einen Typfehler erhalten. Teil zweite Frage war, was passiert, wenn ich eine statisch typisierte Sprache habe? Na ja, in einer statisch typisierten Sprache werde ich, wenn ich diesen Code kompiliere, diesen Typfehler zur compile time bekommen. Das heißt, bevor der Code ausgeführt wird, sehe ich das an der Stelle, was falsch ist. Und jetzt die dritte Frage, was passiert, wenn ich das Ganze in der Sprache mache, die dynamically typed ist? Tja, in dem Fall würde ich auch wieder einen Typfehler bekommen, weil auch in diesen dynamically typed Sprachen gibt es ja für jeden Werten einen bestimmten Typ und zur Laufzeit wird dann überprüft, ob die Art und Weise, wie die Werte miteinander verbunden werden, tatsächlich Typ kompatibel oder Typ korrekt ist. Und in dem Fall würde ich dann also einen Laufzeitfehler kriegen, und zwar auch wieder an der Stelle, wo auch die statisch typisierte Sprache eine Warnung ausgeben würde, nämlich hier, aber eben erst, wenn ich den Code tatsächlich ausführe und tatsächlich an dieser Zeile während der Ausführung ankomme. Ja, und das war es ja schon in diesem zweiten Teil des Moduls Typsysteme. Ich hoffe, Sie haben jetzt einen besseren Eindruck davon, wie Typen und Typsysteme eigentlich in Programmiersprachen eingebettet werden können und in welcher Form dann zum Beispiel Typfehler dem Programmierer mitgeteilt werden, also entweder zum Beispiel beim Compilieren oder vielleicht auch erst zur Laufzeit oder so ein bisschen dazwischen mithilfe dieser Gradual Type Checkers, die wir ja kurz angeschaut haben. Damit danke fürs Zuhören und bis zum nächsten Mal.