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 einfach wie in Racket können wir in Java zwar keine Funktionen (bzw. analog Methoden) übergeben, mit ein paar Tricks ist das jedoch dennoch 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
}
public static int wendeAn(method f, int x){
return f(x);
}
Das funktioniert so jedoch nicht! Um eine Methode zu übergeben, 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 IntUnaryOperator
, mit welchem wir speziell
Methoden mit der Signatur int ‑> int
erstellen können:
(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
IntUnaryOperator
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 - nutzt "import java.util.function.IntUnaryOperator;"
//erhoehe - Methodendeklaration V2
public static IntUnaryOperator erhoehe = (x) -> {
return x + 1;
};
Mit dem Datentyp IntUnaryOperator
haben wir eine Methode bzw. Funktion erzeugt,
die eine ganze Zahl (int 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 IntUnaryOperator
können wir nun an eine andere Methode übergeben - z.B.:
//Java - nutzt "import java.util.function.IntUnaryOperator;"
public static int wendeAn(IntUnaryOperator f, int x){
return f.applyAsInt(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. Die Listenfunktionen kann man in Java jedoch nicht direkt auf Arrays anwenden.
Dafür wandeln wir diese erst zu einem Stream um: Arrays.stream()
und anschließend
wieder zurück zu einem Array: toArray()
.
(a) Überlege, welche Ausgabe die untenstehenden Java-Zeilen produzieren und überprüfe deine Vermutung.
//Java - nutzt "import java.util.Arrays;" und "import java.util.function.IntUnaryOperator;"
public static void main(String[] args) {
int[] zahlen = {1, 2, 3, 4};
System.out.println(Arrays.toString(zahlen));
zahlen = Arrays.stream(zahlen).map(erhoehe).toArray();
System.out.println(Arrays.toString(zahlen));
}
Zur Nutzung der Listenfunktionen haben wir in Racket häufig anonyme Funktionen übergeben, zum Beispiel:
;Racket
(define zahlen (list 1 2 3 4))
(filter (lambda (x) (< x 3)) zahlen)
Auch das ist mittlerweile in Java möglich:
//Java - nutzt "import java.util.Arrays;" und "import java.util.function.IntUnaryOperator;"
public static void main(String[] args) {
int[] zahlen = {1, 2, 3, 4};
zahlen = Arrays.stream(zahlen).filter((x) -> x < 3).toArray();
System.out.println(Arrays.toString(zahlen));
}
(b) Schreibe eine Java-Zeile, die dir in einem Integer-Array jeden Wert um zwei verringert.
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;
int[] noten;
Schuelerin(String name, int jgs, int[] noten){
this.name = name;
this.jgs = jgs;
this.noten = noten;
}
public int durchschnitt(){
if(noten.length == 0) return 0;
int summe = 0;
for (int note : noten) {
summe = summe + note;
}
return summe / noten.length;
}
}
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 int[]{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.