 Ja, herzlich willkommen zur Veranstaltung Programmierparadigmen. Wir schauen uns hier heute funktionale Programmiersprachen an, also Sprachen, bei denen mathematische Funktionen im Zentrum der Programmierung stehen. Und wir werden uns insbesondere eine Sprache dieser Art anschauen, nämlich Schieben und versuchen daran, die Besonderheiten von funktionalen Programmiersprachen ein bisschen besser zu verstehen. Um uns dem Thema so ein bisschen anzunehren, fangen wir erstmal an mit einem Beispiel und zwar in Form einer kleinen Aufwärmübung, die hier ein kleines Stück Schiebencode enthält. Und die Frage für Sie ist, ohne dass Sie jetzt genau wissen, was Schieben eigentlich ist und genau wissen, wie funktionale Programmiersprachen überhaupt funktionieren. Versuche doch mal rauszufinden, zu welchem Wert dieser Code schlussendlich evaluiert wird, also was hier sozusagen rauskommt. Und wenn Sie das sich dann mal überlegt haben, stimmen Sie bitte ab in Ilias und schauen sich erst dann den Rest vom Video an. Schauen wir uns mal die Lösung an, also was hier schlussendlich rauskommt, ist der Wert sieben. Und um zu verstehen, warum, muss man als allererstes mal verstehen, was dieses Lett macht. Was Lett macht ist, es nimmt ein Wert und bindet den an einen Namen und kann das Ganze dann auch mehrmals machen. Also was Lett als Argument hier bekommt, ist eine Liste von Name und Wertpaaren, wobei dann jeweils der Wert, in dem Fall drei, an den Namen, in dem Fall das A gebunden wird und das was da und dieses Binden ist dann aktiv in dem zweiten Argument, das man an dieses Lett übergibt, nämlich in dem Fall hier, an dem Code, der dann dahinter kommt. Was eben genau wichtig ist, ist, was ich gerade gesagt habe, der Scope von diesem Binding, also wo ist das gültig und das ist eben genau nur innerhalb dieses zweiten Argumentes, das wir Lett übergeben, gültig und eben nirgends Nun haben wir in dem Beispiel zweimal Lett und insbesondere wird zweimal etwas dem Namen A zugeordnet, einmal die drei oben und dann die vier im zweiten Lett. Und die Frage ist, welchen Wert haben wir dann, wenn wir ein A jemals lesen. Und was hier passiert ist, dass dieses B, was dann den Wert von A bekommt, sich auf dieses A hier oben bezieht. Denn das zweite Lett, was dem A dann die vier zuordnet, ist nur innerhalb von diesem Stück Code gültig. Das heißt, wir lesen also hier dann das B, was dem B hier entspricht, was dem A hier oben entspricht, also die drei. Und hier lesen wir den Wert von A und das ist dann der Wert vier, der durch das zweite Lett gebunden wird. Also wird schlussendlich drei plus vier gerechnet und da kommt dann sieben raus. Wie gesagt, das war eine kleine Aufwärmübung. Wer das jetzt noch nicht hundertprozentig verstanden hat, kein Problem. Das wird hoffentlich im Rest der Vorlesung alles ein bisschen klarer. Dieses Beispiel, was wir gerade gesehen haben, war ein Beispiel in Scheme und Scheme ist eine typische funktionale Programmiersprache. Was sind überhaupt diese funktionalen Sprachen? Also im Prinzip ist funktionale Programmierung eine Alternative zum imperativen Programmierstil, den die meisten wahrscheinlich bisher hauptsächlich verwendet haben. Und die Kernidee der funktionalen Programmierung, das zeigt sich auch schon im Namen, ist, dass die Berechnung mithilfe von mathematischen Funktionen ausgedrückt werden. Diese mathematischen Funktionen nehmen irgendwelche Eingaben und wie das mathematische Funktionen nun mal machen, produzieren irgendwelche Ausgaben. Und diese Ausgaben sind das, was tatsächlich dann dauerhaft bestehen bleibt. Und insbesondere gibt es zumindest so im reinen funktionalen Paradigma keinen weiteren internen Zustand. Also insbesondere keine variablen oder andere Zustände, die ich überschreiben könnte und Berechnung haben, auch keine weiteren Seiteneffekte. Das heißt, sie können eigentlich auch nichts anderes machen, außer eben aus den gegebenen Eingaben eine Ausgabe berechnen. Das ist so die Theorie und das reine funktionale Paradigma sieht das so vor. In der Praxis ist es natürlich alles ein bisschen verschwommender und die Grenzen zwischen funktionalen Sprachen und nicht funktionalen Sprachen sind eigentlich eher schwammig. Und praktisch ist es eher so, dass es in vielen sogenannten imperativen Sprachen relativ viele funktionale Features gibt. Und das hat sich in den letzten zehn Jahren auch noch mal ein ganzes Stück weiter in die Richtung entwickelt. Zum Beispiel gibt es in vielen Sprachen mittlerweile ja Higher- oder Functions, also die Möglichkeit, eine Funktion als Argument in eine andere Funktion reinzugeben. Also wenn man in JavaScript oder in Java Map aufruft, dann macht man genau das. Man benutzt eben so eine Higher- oder Function und gibt eine andere Funktion als Argument in die erste Funktion rein. Und gleichzeitig gibt es dann auch in vielen sogenannten funktionalen Sprachen doch relativ viele Features, die man eigentlich eher, wenn man jetzt die reine Theorie betrachtet, den imperativen Sprachen erwarten würde. Zum Beispiel Assignments, also dass man variablen Werte zuordnet oder auch Iteration, also dass man Schleifen hat oder durch Datenstrukturen durch iterieren kann. Also in der Praxis sind die Grenzen eher verschwommen. Aber die Grundidee vom Funktionalen-Programmier-Paradigma ist eben, dass man Programmierung mithilfe von mathematischen Funktionen ausdrückt. Schauen wir uns mal ganz kurz an, wo das historisch überhaupt herkommt. Also die Ursprünge der Funktionalen-Programmierung gehen zurück auf die 30er Jahre, was für Informatik ja quasi die Steinzeit ist. Also insbesondere zurück auf den Lambda Calculus, was ein mathematisches Konstrukt ist, das sich Alonso Church damals ausgedacht hat. Die Grundidee vom Lambda Calculus ist, dass man Berechnungen im Prinzip nur mithilfe von zwei sehr einfachen Features oder Sprachfeatures, würde man heute aus Programmiersprachensicht sagen, ausdrücken kann. Nämlich zum einen der Tatsache, dass man Dinge in Funktion abstrahilen kann. Und damit im Lambda Calculus diese Lambda-Notation verwendet. Daher auch der Name, wo ich sage, es gibt eine Funktion, die durch das Lambda ausgedrückt wird. Die bekommt Argumente, nämlich in dem Fall das x und macht dann etwas, und das ist hier dann in dem m ausgedrückt. Und das zweite Konstrukt, wenn man solche Funktionen hat, ist, dass man diese Funktion dann verwendet. Und dieses Verwenden oder Aufrufen von Funktionen sieht im Grunde so aus, dass ich eine Funktion habe, in dem Fall das m und die mit bestimmten Argumenten aufrufe, in dem Fall das n und dadurch passiert dann das, was ich hier schlussendlich erreichen möchte. Aus dem Lambda Calculus haben sich dann die Funktionalen Programmiersprachen entwickelt. Und was wir hier sehen, ist einfach mal eine Liste von Features, die üblicherweise in solchen Funktionalen Programmiersprachen auftreten. Das erste ist die Tatsache, dass wir oft First Class Function values haben und damit dann auch Higher Order Functions. Was das bedeutet, ist schlussendlich, dass Funktionen First Class Citizens in der Sprache sind, also sozusagen Selbstwerte sein können. Und dann kann man mit diesen Funktionen all das tun, was man mit Werten üblicherweise machen kann. Man kann sie insbesondere eben in andere Funktionen als Argument übergeben oder von einem Funktionsaufruf als Rückgabewert bekommen. Und Funktionen, die eben genau das machen, also eine andere Funktion als Argument nehmen, die bezeichnet man auch als Higher Order Functions. Eine zweite Eigenschaft von Funktionalen Programmiersprachen ist, dass Polymorphismus sehr, sehr intensiv verwendet wird. Und insbesondere wird es verwendet, indem eine Funktion eben nicht nur auf einem Typ von Wert agieren kann, sondern überladen ist und mit verschiedenen Typen von Argumenten umgehen kann und je nachdem eben dann entsprechend dieses eine oder dieses andere macht. Und in den Programmiersprachen Implementierungen wird es oft in Hilfe von Typ-Inferenz umgesetzt. Ein drittes typisches Feature von Funktionalen Sprachen ist, dass Listen und Operatoren auf diesen Listen sehr, sehr häufig verwendet werden. Ein Grund, warum das gemacht wird, ist, dass man in den Funktionalen Programmiersprachen gern mit Rekursionen arbeitet. Das bietet sich ganz einfach an, weil die Funktion natürlich so zentral ist in den Sprachen. Und was man auf Listen dann besonders gut machen kann, ist, dass man in der Rekursiven Funktion den ersten Teil der Liste, zum Beispiel das erste Element behandelt und dann rekursiv die Funktion wieder auf dem Rest der Liste aufruft, um schlussendlich dann eine Funktion auf dieser ganzen Liste auszuführen. Der dritte wichtige Punkt hier ist, dass Funktion eben nicht nur einfache Werte zurückgeben, sondern strukturierte Werte, strukturierte Daten zurückgeben können. Zum Beispiel können Funktionen dann natürlich Listen zurückgeben oder eben auch andere Funktionen. Das sechste, naja, fünfte Feature auf der Liste hier ist, dass wir als Teil von Funktionalen Programmiersprachen oft Konstrukturen für strukturierte Objekte direkt in der Sprache drin haben. Also zum Beispiel kann man in quasi jeder Funktionalen Programmiersprache Listen ganz einfach erstellen, indem man die Sonntags der Sprache nimmt, die dafür vorgesehen ist. Also man braucht keine Klassen erstellen, um eine Liste zu erstellen, sondern bestimmte strukturierte Objekte sind einfach Teil der Sprache und können dann auch ganz einfach mithilfe der Sprache direkt konstruiert werden. Und schlussendlich verfügen praktisch alle Funktionalen Programmiersprachen über Garbage Collection und der Grund ist ganz einfach, dass die Art und Weise, wie man in so einer Funktionalen Sprache schreibt, doch dazu führt, dass relativ viele temporäre Daten und Objekte erstellt werden. Und wenn man die explizit dann wieder aufräumen müsste, wäre das relativ umständlich und würde auch so ein bisschen von der Grundidee von Funktionalen Sprachen weggehen, dass man die Dinge mathematisch abstrahiert. Und stattdessen haben diese Sprachen in der Regel Garbage Collection. Das heißt, diese temporären Objekte werden automatisch von der Sprachimplementierung aufgeräumt, sobald sie nicht mehr gebraucht werden. Jetzt gibt es eine ganze Reihe von Funktionalen Sprachen. Und wenn man dieses Paradigma der Funktionalen Sprachen sich jetzt mal anschaut, dann gibt es da im Prinzip zwei Unterklassen. Nämlich zum einen die Porenfunktionalen Sprachen und zum anderen die Nichtporenfunktionalen Sprachen. Die Porenfunktionalen Sprachen nehmen sozusagen dieses Paradigma am ernstesten. Und zwar in dem Sinne, dass Funktionen so wie in der Mathematik ja auch tatsächlich nur von den Parametern, die man in diese Funktion reingibt, abhängen. Und insbesondere gibt es keinen anderen Zustand, also keinen lokalen Zustand oder auch keinen globalen Zustand, der, die das Verhalten und damit auch den Rückgabewert der Funktion beeinflusst, außer eben den Parametern, die man reingibt. Ein Effekt dieser Eigenschaft ist, dass die Reihenfolge, in der Funktionen schlussendlich evaluiert werden, irrelevant ist. Sofern man natürlich immer beachtet, dass, wenn eine Funktion ein Rückgabewert zurückgibt und die dann in eine andere Funktion reingegeben wird, man die erste Funktion vor der anderen ausrechnen muss. Aber solange diese Bedingung erhalten ist, müssen Expressions nicht sofort evaluiert werden, sondern man kann die sogenannte Lazy Evaluation verwenden. Und weil in diesen Porenfunktionen Sprachen ja kein Zustand existiert, der eine Lazy Evaluation vielleicht zu einem anderen Ergebnis führen könnte, als wenn man die Funktion sofort aufruft, hat Lazy Evaluation eben genau dieselbe Bedeutung für die Ausführung wie die Eager Evaluation, also der sofortige Aufruf der Funktion, wenn sie dann im Programm Text dran wäre. Ein Beispiel für eine Sprache, die diesem Porenfunktionalen Paradigma folgt, ist Haskell. Haskell wird schon seit einigen Jahrzehnten von Phil Wortler und anderen Leuten um ihn herum entwickelt und hat seine größte Bedeutung eigentlich als Forschungssprache. Das wird verwendet, um dieses pure funktionale Paradigma weiter voranzutreiben und in der Community wird die Sprache auch sehr aktiv benutzt. In der Praxis hat Haskell jetzt eigentlich keine sehr große Bedeutung, es wird hier und da mal benutzt, aber hat seine große Bedeutung eigentlich als Forschungssprache. Die andere Art funktionale Sprachen, nämlich die nicht puren Funktionalen Sprachen, mixen in die Funktionalen Features und in dieses reine Paradigma der Funktionalen Sprachen noch andere Sprachfeatures rein und insbesondere Assignments, also der Möglichkeit, Werte variablen zuzuordnen und damit einen Zustand zu schaffen, der dann natürlich auch die Evaluation von Funktionen beeinflussen kann und damit quasi das Ergebnis von einer Funktion nicht nur von den Parametern abhängen, sondern eben auch von dem Zustand, den ich mit Hilfe dieser Assignments erstelle. Zwei Beispiele dieser nicht funktionalen Sprachen, die ich hier kurz erwähnen möchte sind Schiem und OCamel. Schiem, wenn wir uns gleich noch ein bisschen genauer anschauen, es ist ein Dialekt von Lisp, einer noch älteren Sprache und Lisp zeichnet sich insbesondere dadurch aus, dass es extrem viele Klammern gibt, also wir werden heute in der Vorlesung viele, viele Klammern zu sehen bekommen. Und die andere Sprache, die auch relativ bekannt ist und eine nicht pure funktionale Sprache ist, ist OCamel, was in Frankreich entwickelt wurde und ML, eine weitere Sprache, nicht Machine Learning, sondern eine Programmiersprache hier mit objektorientierten Features kombiniert und damit auch eine ganz interessante Kombination schafft. Und diese nicht pure funktionale, funktionalen Programmiersprachen werden in der Praxis durchaus auch verwendet, sind nicht ganz so populär wie vielleicht Java oder CI oder Python oder JavaScript, aber haben durchaus ihre Berechtigung und werden in bestimmten Fällen auch verwendet, mehr als die pure funktionale Programmiersprachen. Ja und dann sind wir auch schon am Ende dieses ersten kleinen Einführungsteiles in diese Veranstaltung zum Thema Funktionalesprachen. Weiter geht es dann gleich mit mehr Details zur Sprache scheme, wo wir dann hoffentlich einige dieser Features, die wir gerade gesehen haben, noch ein bisschen genauer verstehen werden. Danke erst mal fürs Zuhören und bis zum nächsten Mal.