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.
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:
- Deterministisch: Für dieselben Argumente liefert sie immer dasselbe Ergebnis. Das Ergebnis hängt nur von den Parametern ab.
- Ohne Seiteneffekte: Sie verändert nichts ausserhalb von sich selbst. Ihre einzige Wirkung ist der Rückgabewert.
// 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.
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 tut | Beispiel in JavaScript | Warum das Probleme macht |
|---|---|---|
| globale Variable verändern | total += m.wh; | der nächste Aufruf startet mit einem falschen Wert |
| Parameter verändern | liste.sort(), obj.status = "x" | der Aufrufer hat plötzlich andere Daten |
| Ausgabe erzeugen | console.log(...), DOM ändern | vermischt Logik und Anzeige, schwer testbar |
| Datei, Datenbank, Netzwerk | fs.writeFileSync(...), fetch(...) | der Test braucht Infrastruktur und kann scheitern |
| globalen Zustand lesen | mwstSatz, config.tarif | Ergebnis hängt von unsichtbarem Zustand ab |
| Zeit und Zufall | Date.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.
Welche markierten Zeilen sind Seiteneffekte oder versteckte Inputs?
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.
Was gibt die letzte Zeile aus?
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.
// 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:
// 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).
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 istmesswerte[0]nicht mehr der erste Wert des Tages, sondern der kleinste.total += m.whverä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:
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.
// 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:
Verbrauch: 1605 Wh
Verbrauch: 1605 Wh
Spitze: 2026-03-02T07:30
Erster Wert: 2026-03-02T07:00Beide 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.
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.
Typische Fehler
- «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.
- Mutierende Array-Methoden auf Parametern.
sort(),reverse(),splice(),push()undpop()verändern das Array des Aufrufers. VerwendetoSorted(),toReversed(), Spread oderfilter. - Debug-Ausgaben in der Logik stehen lassen. Ein
console.logmitten in einer Berechnung ist ein Seiteneffekt und gehört nach dem Debuggen wieder entfernt. Ausgaben macht die Schale. - Zeit im Inneren abfragen.
new Date()oderDate.now()in einer Berechnung machen Tests vom Kalender abhängig. Übergibheuteals Parameter, dann kannst du jeden Stichtag testen. - 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()oderMath.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.