Multiparadigmatische Nutzung von Programmierparadigmen
Rekursion und Schleifenstrukturen
In imperativen Programmen ist die Nutzung von Schleifenstrukturen wie while
und for
ein gängiges Programmierkonzept. Zum Beispiel zur Bestimmung der Fakultät:
//Java
public static int fakultaet(int n){
int ergebnis = 1;
while(n > 1){
ergebnis = ergebnis * n;
n = n-1;
}
return ergebnis;
}
In funktionalen Programmen sind diese Programmierkonzepte jedoch nicht möglich. Anstelle dieser verwendet man
Rekursion.
Aufgabe 1: Rekursion
(a) Ergänze den fehlenden Racket-Code ...
zur Erstellung einer rekursiven Fakultätsberechnung.
;Racket
(define fakultaet
(lambda (n)
(if [= n 0]
1
...)))
Rekursion ist jedoch kein Alleinstellungsmerkmal funktionaler Sprachen, sondern kann in imperativen Sprachen analog verwendet werden.
(b) Schreibe in dein Java-Programm eine Methode fakultaetRek
, die die Fakultätsberechnung rekursiv durchführt.
Funktionen höherer Ordnung und Anonyme Funktionen
Ein weiteres wichtiges Programmierkonzept der funktionalen Programmierung stellen Funktionen höherer Ordnung dar. Also all die Funktionen, die Funktionen als Übergabedaten erhalten oder als Rückgabedaten liefern.
Das funktionale Konzept, Funktionen wie andere Daten zu behandeln und sie entsprechend als Werte übergeben oder zurückgeben zu können, ist so mächtig und praktisch, dass es inzwischen auch in viele imperative Programmiersprachen integriert wurde.
Aufgabe 2: Funktionen höherer Ordnung in Java
Ganz so unkompliziert wie in Racket können wir in Java zwar keine Funktionen (bzw. analog: Methoden) übergebenen – mit ein paar Hilfsmitteln ist es jedoch möglich.
(a) Analysiere den untenstehenden Racket-Code. Welche Ausgabe erwartest du?
;Racket
(define erhoehe
(lambda (x)
(+ x 1)))
(define wendeAn
(lambda (f x)
(f x)))
(wendeAn erhoehe 2)
Möchten wir die gleiche Funktionalität in Java darstellen, wären beispielsweise die folgenden Methoden naheliegend:
//Java
//erhoehe - Methodendeklaration V1
public static int erhoehe(int x){
return x + 1
}
//wendeAn - Methodendeklaration V1
public static int wendeAn(Function f, int x){
return f(x);
}
Das funktioniert so jedoch nicht! Um eine Methode als Übergabedaten nutzen zu können, muss diese Methode von einem besonderen Typ sein: einem Functional Interface. Was es damit genau auf sich hat, ist für uns hier nicht wichtig. Im Folgenden nutzen wir beispielhaft das Functional Interface Function<T,R>.
Im Interface Function<T,R>
gibt
T
den Typ der Übergabedaten an und R
den Typ der Funktionsrückgabe.
Eine Funktion vom Typ Function<Integer,String>
bekommt also Daten vom Typ Integer
und gibt einen String
zurück.
(b) Vergleiche die Methodendeklaration V2 von erhoehe
im untenstehenden Java-Code mit der über dieser Aufgabe
stehenden Methodendeklaration V1. Beantworte die folgenden Fragen:
- Was ersetzt
Function<Integer,Integer>
in der Methodendeklaration? - Wo finden sich die Parameter der beiden Methoden im Code?
- Wo findet sich die Rückgabe der beiden Methoden im Code?
//Java - benötigt "import java.util.function.Function;"
//erhoehe - Methodendeklaration V2
public static Function<Integer,Integer> erhoehe = (x) -> {
return x + 1;
};
Mit dem Typ Function<Integer,Integer>
haben wir eine Methode bzw. Funktion erzeugt,
die eine ganze Zahl (x)
als Eingabe erhält und ebenfalls eine ganze
Zahl mit return x + 1;
zurückgibt.
(c) Schreibe nach demselben Prinzip eine Methode vorzeichenbit
, die beim Erhalt
einer positiven Zahl 0 und beim Erhalt einer negativen Zahl 1 zurückgibt.
Unsere Methoden vom Typ Function<Integer,Integer>
können wir nun an eine andere Methode übergeben - z.B.:
//Java - benötigt "import java.util.function.Function;"
//wendeAn - Methodendeklaration V2
public static int wendeAn (Function<Integer,Integer> f, int x){
return f.apply(x);
}
(d) Rufe die Methode wendeAn
mit den Methoden erhoehe
und vorzeichenbit
auf
und überprüfe die Korrektheit der Aufrufe durch geeignete Ausgaben.
Aufgabe 3: Die Listenfunktionen map und Filter in Java
Neben der allgemeinen Möglichkeit, Funktionen höherer Ordnung zu erstellen, finden sich insbesondere
Listenfunktionen wie map
und filter
explizit in vielen imperativen Sprachen
wieder - darunter auch in Java. Diese Funktionen kann man in Java jedoch nicht direkt auf Listen anwenden,
sondern nur auf
Streams.
Listen lassen sich mit der Methode stream()
zum Glück sehr einfach
in einen Stream umwandeln und nach Verarbeitung mit toList()
auch sehr einfach
wieder zurück zu einer Liste.
(a) Überlege, welche Ausgabe die untenstehenden Java-Zeilen produzieren und überprüfe deine Vermutung.
//Java
public static void main(String[] args) {
List<Integer> zahlenliste = new ArrayList<>(List.of(1, 2, 3, 4));
System.out.println(zahlenliste);
zahlenliste = zahlenliste.stream().map(erhoehe).toList();
System.out.println(zahlenliste);
}
Zur Nutzung der Listenfunktionen haben wir in Racket häufig anonyme Funktionen übergeben, zum Beispiel:
;Racket
(define zahlenliste (list 1 2 3 4))
(filter (lambda (x) (< x 3)) zahlenliste)
Auch das ist mittlerweile in Java möglich:
//Java
public static void main(String[] args) {
zahlenliste = new ArrayList<>(List.of(1, 2, 3, 4));
System.out.println(zahlenliste);
zahlenliste = zahlenliste.stream().filter((x) -> x < 3).toList();
System.out.println(zahlenliste);
}
(b) Schreibe einen Java-Befehl, der dir in einer Liste jeden Wert um zwei verringert. Nutze hierfür eine anonyme Funktion.
Diese Syntaxkonstruktion zur Funktionserstellung nennt sich (wie auch in Racket) ein Lambda‑Ausdruck.
Ein Lambda-Ausdruck implementiert immer ein funktionales Interface.
Der allgemeine Aufbau eines Lambda-Ausdrucks laut:
Besteht der Funktionskörper beispielsweise nur aus einem einzigen Ausdruck (wie in unseren Beispielen),
so sind sowohl die geschweiften Klammern als auch das Schlüsselwort
Auch die runden Klammern um die Parameter können unter bestimmten Umständen weggelassen werden.
Hat die Funktion genau einen Parameter und es ist durch das implementierte Interface eindeutig klar, welchen Datentyp
der Parameter besitzen muss, so sind auch Schreibweisen wie: Zusatzinfo: Der Lambda-Ausdruck in Java
Die in Aufgabe 2 mit dem Code (x) -> {return x + 1;}
und in Aufgabe 3 mit dem Code (x) -> x < 3
genutzte Art der Funktionserstellung
wurde in Java 8 eingeführt, um unter anderem die funktionale Programmierung in Java zu unterstützen.
(Parameter) -> {Funktionskörper}
.
Die Klammerungen können jedoch unter bestimmten Bedingungen weggelassen werden.
return
nicht notwendig.
In Aufgabe 2 wäre damit beispielsweise (x) -> x + 1
ebenso korrekt.
x -> x + 1
zulässig.
Records vs. Klassen und Objekte
Zur Strukturierung von imperativen Programmen kommen häufig Klassen und Objekte zum Einsatz.
Betrachtet man beispielsweise die Klasse Schuelerin
in Java:
//Java
public static class Schuelerin{
String name;
int jgs;
List<Integer> noten;
Schuelerin(String name, int jgs, List<Integer> noten){
this.name = name;
this.jgs = jgs;
this.noten = noten;
}
public double durchschnitt(){
if(noten.isEmpty()) return 0;
int summe = 0;
for (int note : noten) {
summe = summe + note;
}
return (double) summe / noten.size();
}
}
Die Klasse besitzt mit name
, jgs
und noten
drei veränderbare Attribute und mit
durchschnitt
eine Methode, mit welcher der Notendurchschnitt der im Attribut noten
gespeicherten
Notenliste berechnet werden kann.
Eine beispielhafte Verwendung der Klasse in Form des Objekts lea
lässt sich wie folgt umsetzen:
//Java
public static void main(String[] args) {
Schuelerin lea = new Schuelerin("Lea", 12, new ArrayList<>(List.of(13, 3, 11, 9, 8)));
System.out.println(lea.durchschnitt());
}
Aufgabe 4: Records
Im funktionalen Programmierparadigma finden sich keine Klassen und Objekte. Um Daten zu strukturieren und zusammenzusetzen können hier Records genutzt werden:
;Racket
(define-record schuelerin
make-schuelerin
(schuelerin-name string)
(schuelerin-jgs natural)
(schuelerin-note (list-of natural)))
(define lea (make-schuelerin "Lea" 12 (list 13 3 11 9 8)))
Records bieten im Gegensatz zu Klassen keine Möglichkeit, Methoden zu integrieren. Um die Daten der Records zu verarbeiten, müssen entsprechend Funktionen genutzt werden.
(a) Schreibe eine Racket-Funktion durchschnitt-schuelerin
, die den Durchschnitt eines übergebenen
schuelerin
-Records zurückgibt.
Multiparadigmatische Programmiersprachen
Auch wenn die Programmiersprache Java einen klaren Fokus auf die imperativ-objektorientierte Programmierung legt, kann man, wie du gesehen hast, einige funktionale Konzepte auch in Java umsetzen. Viele moderne Programmiersprachen unterstützen mittlerweile Ansätze verschiedener Paradigmen, um eine möglichst flexible Programmierung zu erlauben. Sprachen, die verschiedene Paradigmen zulassen, nennt man daher auch multiparadigmatisch.
Die von uns in diesem Kapitel verwendete Racket-Version erlaubt zwar nur funktionale Konzepte, es gibt jedoch
auch multiparadigmatische Racket-Versionen. So kann man in diesen beispielsweise
mit dem Operator set!
Definitionen verändern oder auch Klassen und Objekte erstellen.