Funktionale Programmierung

Überblick

Dozent: Florian Dahms, R 1-138

Referenzen

Warum funktionale Programmierung?

  • "Simplerer" Code
  • Einfacher parallelisierbar
  • Gut testbarer Code
  • In vielen Fällen das bessere Werkzeug

Warum Clojure / Lisp?

StackOverflow 2022 Developer Survey

Funktionale Programmierung ist nicht grundsätzlich besser, als andere Ansätze, wie z.B. Objekt Orientierte Programmierung (OOP).

Für bestimmte Aufgaben besser geeignet.

Nach dem Kurs könnt ihr (hoffentlich):

  • Komplexe Projekte in Clojure implementieren.
  • Code funktional schreiben, wo es Sinn macht.
  • auch in anderen Sprachen besser programmieren.

Achtung: Vor allem der Einstieg hat etwas mehr Theorie und erfordert etwas Durchhaltevermögen. Später wird der Kurs angewandter.

Installation

  • Hausaufgaben liegen in diesem GitLab Repository
  • Jede Woche gibt es eine neue Hausaufgabe zum bearbeiten.
  • Die Lösung muss als Merge Request vor der nächsten Vorlesung eingecheckt werden.
  • In der folgenden Woche müsst ihr die Lösungen der anderen reviewen (& die nächste Hausaufgabe lösen).

1. Schritt: GitLab einrichten

2. Schritt: Repository auschecken:


          git clone git@gitlab.rlp.net:f.dahms/23ss-fupr-uebungen.git
        

Wenn das Repository schon ausgecheckt ist, stellen wir sicher, dass wir auf dem aktuellen Stand sind:


            git checkout main
            git pull
          

3. Schritt: Einen neuen Branch anlegen:


          git checkout -b neuer_branch
        

4. Schritt: Änderungen commiten:


          git add .
          git commit
        

5. Schritt: Branch pushen


          git push --set-upstream origin neuer_branch
        

Um nach GitLab pushen zu können und einen Merge Request zu eröffnen, müsst ihr entsprechende Rechte haben. Falls euch Rechte fehlen, meldet euch bitte per Teams.

6. Schritt: Merge Request erstellen

  • Mit der Option Merge requests kann in GitLab ein neuer Merge Request für einen Branch erstellt werden.

Der Merge Request muss bis zum nächsten Freitag um 10 Uhr eröffnet sein (direkt vor der Vorlesung).

7. Schritt: Merge Requests von anderen reviewen

Dieser Prozess ist recht nah an der realen Softwareentwicklung in vielen Unternehmen.

Wenn ihr noch keine Übung mit Git habt, ist das eine gute Gelegenheit diese zu bekommen.

Wenn ihr mit einer Aufgabe nicht weiterkommt, scheut euch nicht im Internet nachzuschauen:

Clojure Grundlagen

Clojure Syntax


          (+ 1 2 3)
          (str "Hello " "world" "!!!")
        

          (+ 1 2 3)
        

Eine Anweisung in Clojure (S-expression) ist

  • eine Liste
  • beginnend mit der anzuwendenden Funktion
  • gefolgt von den Parametern der Funktion

Die Prefix Notation ist in Clojure sehr konsistent. In anderen Sprachen kennt man eine Mischung aus Prefix und Infix Notation, sowie jeweils unterschiedlich platzierte Klammern.

Beispiel in Java:


          1 + 2 + 3
          "Hello ".concat("world", "!!!")
        

Parameter einer Expression können wieder Expressions sein:


          (* (+ 2
                (* 4 6))
             (+ 3 5 7))
        

Um eine Expression zu evaluieren müssen wir rekursiv die Subexpressions evaluieren.

Die Subexpressions bilden einen Baum:


                (* (+ 2
                      (* 4 6))
                   (+ 3 5 7))
              
  • Diese Baumstruktur nennt sich "Abstract Syntax Tree" (AST).
  • Praktisch jede Sprache parsed den Source Code in einen AST.
  • In LISP Syntax können wir den AST recht direkt editieren.

LISP Programmierer schätzen die Konsistenz und Einfachheit der Syntax.


https://xkcd.com/297/

Wir können Werten Namen geben:


          (def message "Hallo Welt!")
          (def pi 3.14159)
        

Namen können wir in Expressions verwenden und mit Expressions definieren:


          (def pi 3.14159)
          (def radius 10)
          (def circumference (* 2 pi radius))
        

Namen sind keine Variablen. Wir können den Wert eines Namens überschreiben:


          (def a 5)
          (def a 7)
        

Das ist schlechter Stil! Ein Name sollte nur genau einmal vergeben werden und dann konstant bleiben.

Klassische Variablen sind in der funktionalen Programmierung nicht vorgesehen.
(in der reinen Lehre zumindest)

Wir können eigene Funktionen definieren:


          (defn square [x] (* x x))
          (square 7)
        

Es werden erst alle Parameter evaluiert und dann die Funktion aufgerufen


          (square (+ 3 4))
        

$\Downarrow$


          (square 7)
        

$\Downarrow$


          (* 7 7)
        

Alternativ wäre auch denkbar die Funktion erst zu substituieren:


          (square (+ 3 4))
        

$\Downarrow$


          (* (+ 3 4) (+ 3 4))
        
  • Parameter werden doppelt evaluiert.
  • Funktioniert nicht mit Rekursion.

Wir können Bedingungen formulieren:


          (def x 101)
          (if (> x 100)
              "Eine echt große Zahl"
              "Eine ganz nette Zahl")
        

Jede Expression hat immer einen Wert, zu dem sie evaluiert.

"if" hat als Parameter drei Expressions. Basierend auf dem Wert der ersten gibt es entweder die zweite oder die dritte zurück.

Die Parameter von "if" werden "lazy" evaluiert:


          (if true (/ 6 2) (/ 3 0))
          ; => 3
        

          (if false (/ 6 2) (/ 3 0))
          ; => java.lang.ArithmeticException
        

Später bauen wir solche Funktionen auch selber.

Andere Funktionen die auch lazy evaluiert werden:


          (and (> 4 0) false (/ 3 0))
        

          (or (> 4 0) false (/ 3 0))
        

Wir können den "else" Parameter auch weglassen:


          (if (< 4 0) "Foo")
          ; => nil
        

"nil" ist ein spezieller Wert - quasi die Abwesenheit eines anderen Wertes.

Was tun wir, wenn wir mehrere Anweisungen zusammenfassen wollen?


          (defn abs [x]
            (if (> 0 x)
                (do (println x "is negative - changing sign")
                    (- x))
                x))
          (abs -4)
        

"do" evaluiert zum dem letzten Wert seiner Parameter

Jeder Wert kann im ersten Parameter von "if" verwendet werden.

  • "false" und "nil" sind falsey $\Rightarrow$ der 3. Parameter wird evaluiert.
  • Alle anderen Werte sind truthy $\Rightarrow$ der 2. Parameter wird evaluiert.

Wir können explizit nach "nil" fragen:


          (nil? nil)
          ; => true
        

          (nil? false)
          ; => false
        

"or" gibt den ersten truthy Wert zurück und den letzten Wert, wenn es keinen solchen gibt:


          (or false "Foo" nil)
          ; => "Foo"
        

          (or false nil)
          ; => nil
        

Analog gibt "and" den ersten falsey Wert zurück und Alternativ den letzten.