scalatutorial.de

Stufe 4Funktional denken · Lektion 19 von 34

Auf dem Weg zur funktionalen Programmierung

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 1/6 dieser Stufe

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

Bausteine funktionaler Programmierung und was sie zusammen ergebenreine Funktiongleiche Eingabeunveränderlichval, List, case classAusdrückeif, match liefern WerteKompositionmap, flatMap, andThentestbarnachvollziehbar
Drei Grundideen, ein Ziel: Code, der sich zusammensetzen und prüfen lässt.

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.

 Scala 3
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.kunde

Das funktioniert, verteilt die Logik aber über veränderliche Variablen. Wer den Code liest, muss die Schleife gedanklich durchspielen.

Schritt 1: filtern statt verzweigen

 Scala 3
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

 Scala 3
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.

 Scala 3
val umsatz = betraege.foldLeft(BigDecimal(0))(_ + _)   // 57

Mit 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

 Scala 3
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:

 Scala 3
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.

 Scala 3
@main def bericht(): Unit =
  val daten = ladeBestellungen()              // Nebenwirkung: lesen
  val (umsatz, kunden) = auswertung(daten)    // rein
  println(s"Umsatz: $umsatz, Kunden: ${kunden.mkString(", ")}")   // Nebenwirkung: schreiben

Bibliotheken 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.

Passt dazu