Modulseite

Lektion 1 von 9

Reine Funktionen: Schluss mit versteckten Nebenwirkungen

Du erkennst Seiteneffekte und versteckte Inputs, schreibst deterministische Funktionen und trennst Berechnung von Ein- und Ausgabe nach dem Muster funktionaler Kern, imperative Schale.

ca. 40 Min.0/4 Checks gelöst

Worum es geht

Samira ist im 3. Lehrjahr als Informatikerin Applikationsentwicklung bei der Energiewerk Seetal AG in Hochdorf. Seit alle Haushalte Smart Meter haben, berechnet ein Skript die Monatsrechnungen aus Viertelstundenwerten. An einem Dienstagmorgen meldet sich der Kundendienst: Einer Kundin wurde für März fast doppelt so viel Strom verrechnet wie im Februar. Samira startet die Auswertung für diesen Zähler im Testsystem und bekommt für einen kleinen Ausschnitt 1605 Wattstunden. Korrekt. Sie startet sie ein zweites Mal, ohne irgendetwas zu ändern: 3210 Wattstunden.

Dasselbe Programm, dieselben Daten, ein anderes Ergebnis. Solche Fehler gehören zu den teuersten überhaupt: Sie treten nur manchmal auf, lassen sich schwer nachstellen, und ein Test, der eben noch grün war, ist beim nächsten Lauf rot. Die Ursache ist fast immer dieselbe: Eine Funktion hängt von etwas ab oder verändert etwas, das nicht in ihrer Parameterliste steht.

In dieser Lektion lernst du den wichtigsten Grundgedanken der funktionalen Programmierung kennen: reine Funktionen. Sie sind das Fundament für alles, was in diesem Kurs folgt, von map und reduce über Komposition bis zur Rekursion.

Was eine reine Funktion ausmacht

Denk an einen Taschenrechner. Du tippst 7 × 8 und bekommst 56. Heute, morgen, nach hundert anderen Rechnungen: immer 56. Der Taschenrechner merkt sich nichts von früheren Rechnungen und verändert nichts in deiner Umgebung. Ganz anders der Kaffeeautomat im Pausenraum: Derselbe Knopf liefert manchmal einen Kaffee, manchmal «Bitte Wasser nachfüllen», und nebenbei zählt er mit, wie viele Tassen schon herausgekommen sind.

Eine reine Funktion (pure function) verhält sich wie der Taschenrechner. Sie erfüllt zwei Bedingungen:

  1. Deterministisch: Für dieselben Argumente liefert sie immer dasselbe Ergebnis. Das Ergebnis hängt nur von den Parametern ab.
  2. Ohne Seiteneffekte: Sie verändert nichts ausserhalb von sich selbst. Ihre einzige Wirkung ist der Rückgabewert.
JavaScript
// rein: Ergebnis hängt nur von den Parametern ab, nichts wird verändert
function bruttoAusNetto(netto, mwstSatz) {
  return netto * (1 + mwstSatz / 100);
}

// unrein: liest einen versteckten Wert von aussen
let mwstSatz = 8.1;
function brutto(netto) {
  return netto * (1 + mwstSatz / 100); // was, wenn jemand mwstSatz ändert?
}

// unrein: verändert das Objekt des Aufrufers
function rabattGeben(bestellung) {
  bestellung.total = bestellung.total * 0.9; // die Bestellung draussen ist jetzt anders
  return bestellung.total;
}

brutto(100) liefert heute 108.1. Setzt irgendein anderer Programmteil mwstSatz auf 2.6, liefert genau derselbe Aufruf etwas anderes, obwohl am Aufruf selbst nichts anders aussieht. Und wer rabattGeben(bestellung) zweimal aufruft, gibt 19 % statt 10 % Rabatt.

Merk dir zwei Begriffe: Eine Funktion, die etwas liest, das nicht als Parameter hereinkommt, hat einen versteckten Input. Eine Funktion, die etwas verändert, hat einen versteckten Output. Reine Funktionen haben weder noch: Alles, was hinein- und herausgeht, steht in der Signatur.

Check 1 · Eine AntwortEinstieg

Welche dieser Funktionen ist rein?

Seiteneffekte erkennen

Ein Seiteneffekt ist jede Wirkung einer Funktion, die über ihren Rückgabewert hinausgeht. Die folgende Tabelle ist deine Checkliste, wenn du Code auf Reinheit prüfst:

Was die Funktion tutBeispiel in JavaScriptWarum das Probleme macht
globale Variable veränderntotal += m.wh;der nächste Aufruf startet mit einem falschen Wert
Parameter verändernliste.sort(), obj.status = "x"der Aufrufer hat plötzlich andere Daten
Ausgabe erzeugenconsole.log(...), DOM ändernvermischt Logik und Anzeige, schwer testbar
Datei, Datenbank, Netzwerkfs.writeFileSync(...), fetch(...)der Test braucht Infrastruktur und kann scheitern
globalen Zustand lesenmwstSatz, config.tarifErgebnis hängt von unsichtbarem Zustand ab
Zeit und ZufallDate.now(), Math.random()jeder Aufruf liefert etwas anderes

Die letzten beiden Zeilen sind streng genommen keine Seiteneffekte, sondern versteckte Inputs. Sie machen eine Funktion aber genauso unrein, weil sie nicht mehr deterministisch ist. Die Lösung ist fast immer gleich: Mach den Wert zum Parameter. Statt istUeberfaellig(rechnung) mit new Date() im Inneren schreibst du istUeberfaellig(rechnung, heute).

Es gibt einen einfachen Test, ob eine Funktion rein ist: Kannst du jeden Aufruf durch sein Ergebnis ersetzen, ohne dass sich das Programm anders verhält? Bei bruttoAusNetto(100, 8.1) darfst du überall 108.1 hinschreiben. Diese Eigenschaft heisst referenzielle Transparenz. Beim alten summiereVerbrauch() geht das nicht: Der erste Aufruf «ist» 1605, der zweite 3210.

Check 2 · Mehrere AntwortenEinstieg

Welche markierten Zeilen sind Seiteneffekte oder versteckte Inputs?

JavaScript
let anzahlRechnungen = 0;

function erstelleRechnung(kunde, positionen) {
  const total = positionen.reduce((s, p) => s + p.betrag, 0); // A
  anzahlRechnungen++;                                         // B
  kunde.letzteRechnung = total;                               // C
  const datum = new Date().toISOString();                     // D
  const text = `${kunde.name}: CHF ${total}`;                 // E
  return { text, datum };
}

Wähle alle zutreffenden Antworten.

Check 3 · BerechnenFortgeschritten

Was gibt die letzte Zeile aus?

JavaScript
let rabattAktiv = true;
let bestellungen = 0;

function preis(betrag) {
  bestellungen++;
  if (bestellungen > 2) rabattAktiv = false;
  return rabattAktiv ? betrag * 0.9 : betrag;
}

console.log(preis(100) + preis(100) + preis(100));

Funktionaler Kern, imperative Schale

Ein Programm ganz ohne Seiteneffekte wäre nutzlos: Irgendwann muss die Rechnung in die Datenbank und das PDF in den Versand. Das Ziel ist deshalb nicht, Seiteneffekte abzuschaffen, sondern sie an den Rand zu schieben. In der Mitte liegt ein funktionaler Kern aus reinen Funktionen, aussen eine dünne imperative Schale, die liest, schreibt und anzeigt.

Ein Beispiel aus einem Treuhandbüro in Zug: Die Lohnsoftware hat eine Funktion, die alles auf einmal macht.

JavaScript
// vorher: Laden, Konfiguration lesen, Rechnen und Speichern in einer Funktion
function berechneNetto(mitarbeiterId) {
  const brutto = db.ladeLohn(mitarbeiterId);                  // liest Datenbank
  const abzuege = brutto * (config.ahvSatz + config.alvSatz);  // liest globale Konfiguration
  const netto = brutto - abzuege;
  db.speichereNetto(mitarbeiterId, netto);                     // schreibt Datenbank
  return netto;
}

Um diese Funktion zu testen, brauchst du eine Datenbank mit Testdaten und eine passende Konfiguration. Nach dem Umbau sieht es so aus:

JavaScript
// Kern: rein, alle Werte kommen als Parameter
function berechneNetto(brutto, saetze) {
  const abzuege = brutto * (saetze.ahv + saetze.alv);
  return brutto - abzuege;
}

// Schale: holt die Daten, ruft den Kern auf, speichert
function lohnlaufFuer(mitarbeiterId) {
  const brutto = db.ladeLohn(mitarbeiterId);
  const netto = berechneNetto(brutto, config.saetze);
  db.speichereNetto(mitarbeiterId, netto);
}

// Test des Kerns: keine Datenbank, keine Konfiguration nötig (Testwerte frei gewählt)
test("zieht die Beiträge vom Bruttolohn ab", () => {
  expect(berechneNetto(6000, { ahv: 0.05, alv: 0.01 })).toBeCloseTo(5640);
});

Die Schale ist jetzt so dünn, dass sie kaum Fehler enthalten kann. Die eigentliche Logik steckt im Kern und lässt sich mit beliebig vielen Fällen in Millisekunden testen.

Schritt für Schritt: Samiras Summe reparieren

So sieht der betroffene Teil des alten Skripts aus. Die Beispieldaten sind vier Viertelstundenwerte in Wattstunden (Wh). Wir rechnen bewusst mit ganzen Wattstunden, weil Kommazahlen in JavaScript kleine Rundungsfehler haben (0.1 + 0.2 ergibt 0.30000000000000004).

JavaScript
let total = 0;

function summiereVerbrauch(messwerte) {
  messwerte.sort((a, b) => a.wh - b.wh);
  for (const m of messwerte) {
    total += m.wh;
  }
  console.log("Total: " + total);
  return total;
}

Schritt 1: Seiteneffekte markieren. Samira geht Zeile für Zeile durch und fragt bei jeder: Liest oder verändert sie etwas ausserhalb der Funktion?

  • messwerte.sort(...) sortiert das Array des Aufrufers um. Danach ist messwerte[0] nicht mehr der erste Wert des Tages, sondern der kleinste.
  • total += m.wh verändert eine globale Variable, die nie zurückgesetzt wird.
  • console.log(...) ist eine Ausgabe.

Schritt 2: Den Fehler mit einem Test festhalten. Bevor sie etwas ändert, schreibt sie einen Test, der rot wird:

JavaScript
test("liefert beim zweiten Aufruf dasselbe Ergebnis", () => {
  const daten = [
    { zeit: "2026-03-02T07:00", wh: 420 },
    { zeit: "2026-03-02T07:15", wh: 380 },
  ];
  expect(summiereVerbrauch(daten)).toBe(800);
  expect(summiereVerbrauch(daten)).toBe(800); // scheitert heute: 1600
});

Schritt 3: Globalen Zustand lokal machen. Die Summe entsteht bei jedem Aufruf neu in einer lokalen Variable.

Schritt 4: Die Ausgabe in die Schale verschieben. Wer das Ergebnis anzeigen will, macht das mit dem Rückgabewert.

Schritt 5: Nicht mehr sortieren. Fürs Summieren ist die Reihenfolge egal. Wo eine sortierte Liste wirklich gebraucht wird, etwa für den Spitzenwert, entsteht eine Kopie mit toSorted (seit ES2023, in älteren Umgebungen [...messwerte].sort(...)).

Schritt 6: Zusammensetzen und prüfen.

JavaScript
// Kern: reine Funktionen
function summiereVerbrauch(messwerte) {
  let summe = 0;                 // lokal: entsteht bei jedem Aufruf neu
  for (const m of messwerte) {
    summe += m.wh;
  }
  return summe;
}

function spitzenwert(messwerte) {
  // toSorted erzeugt eine sortierte KOPIE, das Original bleibt unverändert
  const sortiert = messwerte.toSorted((a, b) => b.wh - a.wh);
  return sortiert[0];
}

// Schale: Daten bereitstellen und ausgeben
const maerz = [
  { zeit: "2026-03-02T07:00", wh: 420 },
  { zeit: "2026-03-02T07:15", wh: 380 },
  { zeit: "2026-03-02T07:30", wh: 510 },
  { zeit: "2026-03-02T07:45", wh: 295 },
];
console.log("Verbrauch:", summiereVerbrauch(maerz), "Wh");
console.log("Verbrauch:", summiereVerbrauch(maerz), "Wh");
console.log("Spitze:", spitzenwert(maerz).zeit);
console.log("Erster Wert:", maerz[0].zeit);

Ausgabe:

Text
Verbrauch: 1605 Wh
Verbrauch: 1605 Wh
Spitze: 2026-03-02T07:30
Erster Wert: 2026-03-02T07:00

Beide Aufrufe liefern 1605, der Spitzenwert stimmt, und die Originalliste beginnt immer noch mit dem ersten Wert des Tages. Der Test aus Schritt 2 ist grün. In Lektion 5 wirst du die Schleife durch ein einziges reduce ersetzen. Rein ist sie aber schon jetzt.

Check 4 · Code-LaborEinstieg

Code-Labor: Samiras Summe reparieren. Das alte Skript liefert beim zweiten Aufruf das doppelte Total und bringt die Reihenfolge der Messwerte durcheinander. Mach summiereVerbrauch(messwerte) und spitzenwert(messwerte) rein: keine globale Variable, keine Veränderung der übergebenen Liste, keine Ausgabe in den Funktionen. spitzenwert liefert den Messwert mit dem grössten wh, bei leerer Liste null.

Code-Labor · JavaScript
Strg + Enter

Typische Fehler

  1. «Nur lesen ist harmlos.» Eine Funktion, die eine globale Konfiguration liest, ist nicht rein: Ändert jemand die Konfiguration, ändert sich das Ergebnis. Gib den Wert als Parameter mit.
  2. Mutierende Array-Methoden auf Parametern. sort(), reverse(), splice(), push() und pop() verändern das Array des Aufrufers. Verwende toSorted(), toReversed(), Spread oder filter.
  3. Debug-Ausgaben in der Logik stehen lassen. Ein console.log mitten in einer Berechnung ist ein Seiteneffekt und gehört nach dem Debuggen wieder entfernt. Ausgaben macht die Schale.
  4. Zeit im Inneren abfragen. new Date() oder Date.now() in einer Berechnung machen Tests vom Kalender abhängig. Übergib heute als Parameter, dann kannst du jeden Stichtag testen.
  5. Lokale Veränderung für unrein halten. Eine Schleife, die eine lokale Variable hochzählt, macht eine Funktion nicht unrein. Entscheidend ist, was von aussen sichtbar ist.

Zusammenfassung

  • Eine reine Funktion liefert für gleiche Eingaben immer dasselbe Ergebnis und hat keine Seiteneffekte.
  • Seiteneffekte sind z.B. globale Variablen verändern, Parameter mutieren, Ausgaben, Datei- und Datenbankzugriffe.
  • Versteckte Inputs wie globale Konfiguration, Date.now() oder Math.random() machen eine Funktion ebenfalls unrein. Mach sie zu Parametern.
  • Referenzielle Transparenz: Einen reinen Aufruf kannst du überall durch sein Ergebnis ersetzen.
  • Funktionaler Kern, imperative Schale: Logik rein in der Mitte, Ein- und Ausgabe dünn am Rand.
  • Lokale Variablen innerhalb einer Funktion zu verändern ist erlaubt, solange nichts davon nach aussen dringt.

Mit deiner eigenen KI vertiefen

Kopiere einen Prompt in Claude, ChatGPT oder Claude Code. Er macht die KI zur Lernbegleitung statt zum Lösungsautomaten.

Seiteneffekt-Detektiv Chat-KI

Du übst, Seiteneffekte und versteckte Inputs in fremdem Code zu finden. Die KI gibt dir Code und stellt Fragen, statt die Lösung zu verraten. Ideal nach Lektion 1 und 2.