Stufe 4Funktional denken · Lektion 19 von 34
Auf dem Weg zur funktionalen Programmierung
Funktionale Programmierung klingt nach Mathematik, ist im Kern aber eine Handvoll praktischer Regeln: Funktionen ohne versteckte Nebenwirkungen, Daten, die sich nicht ändern, und kleine Bausteine, die sich zusammenstecken lassen. Diese Lektion baut ein imperatives Programm schrittweise um.
Die Grundideen
- Reine Funktionen: Das Ergebnis hängt nur von den Argumenten ab, und die Funktion verändert nichts außerhalb ihrer selbst – keine globalen Variablen, keine Ausgabe, keine Datei. Bei gleicher Eingabe kommt immer dasselbe heraus.
- Unveränderliche Daten: Statt ein Objekt zu verändern, erzeugt man ein neues.
val,Listund case classes mitcopysind die Werkzeuge dafür. - Ausdrücke statt Anweisungen:
if,matchund Blöcke liefern Werte. Man beschreibt, was herauskommen soll, nicht welche Variable wann gesetzt wird.
Der Lohn: Reine Funktionen lassen sich isoliert testen, gefahrlos parallel ausführen und beliebig kombinieren. Man kann einen Aufruf gedanklich durch sein Ergebnis ersetzen – das nennt man referenzielle Transparenz.
Ein imperativer Ausgangspunkt
Aufgabe: Aus einer Liste von Bestellungen sollen der Gesamtumsatz aller bezahlten Bestellungen über 20 Euro und die Namen der Kunden ermittelt werden.
case class Bestellung(kunde: String, betrag: BigDecimal, bezahlt: Boolean)
val bestellungen = List(
Bestellung("Ada", 35, true),
Bestellung("Alan", 12, true),
Bestellung("Grace", 80, false),
Bestellung("Ada", 22, true)
)
// imperativ
var umsatz = BigDecimal(0)
val kunden = scala.collection.mutable.Set[String]()
for b <- bestellungen do
if b.bezahlt && b.betrag > 20 then
umsatz += b.betrag
kunden += b.kundeDas funktioniert, verteilt die Logik aber über veränderliche Variablen. Wer den Code liest, muss die Schleife gedanklich durchspielen.
Schritt 1: filtern statt verzweigen
val relevante = bestellungen.filter(b => b.bezahlt && b.betrag > 20)Eine Zeile beschreibt, welche Bestellungen gemeint sind. Kein Zähler, kein if im Schleifenrumpf.
Schritt 2: umwandeln mit map
val betraege = relevante.map(_.betrag) // List(35, 22)
val namen = relevante.map(_.kunde).toSet // Set(Ada)Schritt 3: zusammenfassen mit fold
sum gibt es schon fertig. Das allgemeine Werkzeug dahinter ist foldLeft: Es startet mit einem Anfangswert und verbindet ihn Element für Element mit einer Funktion.
val umsatz = betraege.foldLeft(BigDecimal(0))(_ + _) // 57Mit foldLeft lässt sich fast jede Schleife ausdrücken, die einen Wert „aufsammelt“ – Summen, Maxima, Zähler, sogar das Aufbauen einer Map.
Schritt 4: in Funktionen verpacken
def relevant(b: Bestellung): Boolean = b.bezahlt && b.betrag > 20
def auswertung(bs: List[Bestellung]): (BigDecimal, Set[String]) =
val treffer = bs.filter(relevant)
(treffer.map(_.betrag).sum, treffer.map(_.kunde).toSet)auswertung ist rein: Sie liest nur ihre Eingabe und liefert ein Ergebnis. Ein Test braucht keine Vorbereitung und keinen Aufräumschritt – Eingabe hinein, Ergebnis prüfen, fertig.
Funktionen zusammensetzen
Funktionen lassen sich wie Bausteine verketten:
val netto: BigDecimal => BigDecimal = _ / BigDecimal("1.19")
val runden: BigDecimal => BigDecimal = _.setScale(2, BigDecimal.RoundingMode.HALF_UP)
val alsText: BigDecimal => String = b => s"$b €"
val nettoAnzeige = netto andThen runden andThen alsText
nettoAnzeige(BigDecimal(57)) // "47.90 €"andThen führt Funktionen nacheinander aus, compose in umgekehrter Reihenfolge. Aus kleinen, einzeln prüfbaren Teilen entsteht so eine Verarbeitungskette.
Und die Nebenwirkungen?
Ein Programm ohne jede Nebenwirkung wäre nutzlos – irgendwann muss es Dateien lesen, Daten speichern, etwas anzeigen. Funktionale Programmierung verbietet das nicht, sondern schiebt es an den Rand: Der Kern besteht aus reinen Funktionen, außen herum liegt eine dünne Schicht, die Ein- und Ausgabe erledigt.
@main def bericht(): Unit =
val daten = ladeBestellungen() // Nebenwirkung: lesen
val (umsatz, kunden) = auswertung(daten) // rein
println(s"Umsatz: $umsatz, Kunden: ${kunden.mkString(", ")}") // Nebenwirkung: schreibenBibliotheken wie Cats Effect oder ZIO gehen einen Schritt weiter und beschreiben sogar Ein- und Ausgabe als Werte. Das ist ein fortgeschrittenes Thema; für den Einstieg reicht das Prinzip „reiner Kern, dünne Schale“.
Fehlende Werte und Fehler funktional
Was, wenn ein Wert fehlen kann oder ein Schritt scheitert? Statt null und Exceptions verwendet funktionales Scala Typen, die diese Fälle ausdrücken. Sie sind Thema der nächsten Lektionen: Option, Try und Either. Was sie gemeinsam haben, erklärt anschließend die Lektion über Monaden.