Home Fp Basics Python Artikel 4
Teil 4 Funktionale Programmierung & Kategorientheorie

Komplexität verlagern – Von if/else-Ketten zu funktionaler Komposition

Bild: KI-generiert


Komplexität verlagern – Von if/else-Ketten zu funktionaler Komposition

Wie Monaden, Komposition und reine Funktionen eine 100-Zeilen-Methode in eine lesbare Pipeline verwandeln.


Die zentrale These dieser Artikelreihe:

Wenn man eine Datenstruktur hat, die einen bestimmten Zustand im System repräsentiert, und man zeigen kann, dass diese ein Funktor / Applicative / Monad ist, kann man Dinge, die sonst eine lange if/else/switch-belastete Methode sind, in eine Kombination von Funktionen verwandeln.

In diesem Artikel wird genau das umgesetzt – an einem Bestellverarbeitungs-System.

1. Das Problem: Eine typische Bestellverarbeitung

Anforderungen: 1. E-Mail-Adresse des Kunden validieren 2. Prüfen, ob der Warenkorb nicht leer ist 3. Jeden Artikel gegen den Produktkatalog prüfen 4. Einen optionalen Rabattcode anwenden 5. Den Mindestbestellwert prüfen 6. Die Bestellung zusammenbauen

Was in der Praxis typischerweise entsteht:

def process_order_imperative(customer_email, items, discount_code=None):
    if not customer_email or "@" not in customer_email:
        return {"status": "error", "message": "Ungültige E-Mail-Adresse"}
    if "." not in customer_email.split("@")[-1]:
        return {"status": "error", "message": "Ungültige E-Mail-Adresse"}
    email = customer_email.strip().lower()
    if not items:
        return {"status": "error", "message": "Warenkorb ist leer"}
    # ... 25 Zeilen Artikelvalidierung ...
    if discount_code is not None:
        if discount_code not in DISCOUNT_CODES:
            return {"status": "error", "message": f"Ungültiger Rabattcode"}
    if final_total < 10.0:
        return {"status": "error", "message": "Mindestbestellwert nicht erreicht"}
    return {"status": "success", "order": {...}}

Elf if-Statements. Die Probleme: 1. Verschachtelte Kontrollflüsse: Happy Path und Fehlerbehandlung sind vermischt 2. Schwer testbar: Nur die gesamte Funktion testbar, nicht die einzelnen Schritte 3. Schwer erweiterbar: Jede neue Anforderung bedeutet ein weiteres if 4. Keine Wiederverwendung: Die E-Mail-Validierung ist in dieser Funktion gefangen

"Many code bases are completely dominated by error handling. [...] it is nearly impossible to see what the code does because of all of the scattered error handling." — Robert C. Martin: Clean Code, S. 134

2. Die Idee: Railway Oriented Programming

"In this approach, the top track is the happy path, and the bottom track is the failure path. You start off on the success track, and if you're lucky you stay on it to the end." — Scott Wlaschin: Domain Modeling Made Functional, S. 205

validate_email → validate_cart → validate_items → apply_discount → check_total → build_order
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ → Ok(Order)
      ↓               ↓               ↓               ↓              ↓
──────────────────────────────────────────────────────────────────────────────── → Err(msg)

Jede Funktion ist eine Weiche: Sie kann auf der Erfolgs-Schiene bleiben oder auf die Fehler-Schiene wechseln. Sobald ein Fehler auftritt, werden alle weiteren Schritte automatisch übersprungen.

Der Adapter: bind.

"The adapter that converts switch functions to two-track functions is a very important one — it's commonly called bind or flatMap." — Scott Wlaschin: Domain Modeling Made Functional, S. 206

3. Schritt 1: Immutable Datentypen definieren

from dataclasses import dataclass

@dataclass(frozen=True)
class Product:
    product_id: str
    name: str
    price: float
    stock: int

@dataclass(frozen=True)
class OrderItem:
    product_id: str
    name: str
    quantity: int
    unit_price: float
    line_total: float

@dataclass(frozen=True)
class Order:
    email: str
    items: tuple[OrderItem, ...]
    subtotal: float
    discount_code: str | None
    discount_percent: float
    discount_amount: float
    total: float

4. Schritt 2: Reine Validierungsfunktionen extrahieren

Jede Funktion hat die Signatur A → Result[B]:

def validate_email(email: str) -> Result:
    if not email or "@" not in email:
        return Err("Ungültige E-Mail-Adresse")
    if "." not in email.split("@")[-1]:
        return Err("Ungültige E-Mail-Adresse")
    return Ok(email.strip().lower())

def validate_cart(items: list[dict]) -> Result:
    if not items:
        return Err("Warenkorb ist leer")
    return Ok(items)

Jede Funktion ist: - Pure: Kein Zustand, keine Seiteneffekte - Isoliert testbar - Wiederverwendbar - Eine Verantwortlichkeit

5. Schritt 3: Die Pipeline zusammensetzen

def process_order_functional(customer_email, items, discount_code=None):
    return (
        validate_email(customer_email)
        .bind(lambda email:
            validate_cart(items)
            .bind(validate_all_items)
            .bind(lambda validated_items:
                apply_discount(discount_code, validated_items))
            .bind(check_minimum_total)
            .map(lambda order_data:
                build_order(email, order_data))
        )
    )

Dieselbe Logik – aber lies es laut: 1. Validiere die E-Mail 2. dann validiere den Warenkorb 3. dann validiere alle Artikel 4. dann wende den Rabatt an 5. dann prüfe den Mindestbestellwert 6. dann baue die Bestellung

Kein einziges if für Fehlerbehandlung. Die Result-Monade kümmert sich darum.

"What really is mind blowing is that all these problems may be solved using the same clever trick: turning to embellished functions." — Bartosz Milewski: Category Theory for Programmers, S. 330

bind vs. map – Die Faustregel

Operation Zweck Signatur
bind Schritt, der fehlschlagen kann A → Result[B]
map Reine Transformation, die nie fehlschlägt A → B

6. Der Vergleich

Eigenschaft Imperativ Funktional
Fehlerbehandlung 11 if-Statements Automatisch durch Result-Monade
Testbarkeit Nur Gesamtfunktion Jeder Schritt isoliert
Erweiterbarkeit Neues if in der Mitte Neues bind in der Pipeline
Rückgabetyp dict (was ist drin?) Result[Order] (typisiert)
Wiederverwendung Keine Jede Validierung separat nutzbar

7. Schritt 4: Erweiterbarkeit durch Komposition

Neues Feature: "Für B2B-Kunden sollen zusätzliche Regeln gelten."

Funktional: Neue Funktionen komponieren

def check_no_duplicates(items: list[dict]) -> Result:
    seen = set()
    for item in items:
        pid = item.get("product_id")
        if pid in seen:
            return Err(f"Doppelter Artikel: {pid}")
        seen.add(pid)
    return Ok(items)

def check_max_items(max_count: int):
    def validator(items: list[dict]) -> Result:
        if len(items) > max_count:
            return Err(f"Maximal {max_count} verschiedene Artikel erlaubt")
        return Ok(items)
    return validator

Higher-Order Functions die Closures zurückgeben – parametrisierte Validierungsregeln.

"To replace the strategy classes, we use higher-order functions that implement the needed algorithms." — Stuart Sierra & Luke VanderHart: Functional Patterns, S. 106

Regeln komponieren

def compose_validators(*validators):
    def composed(value):
        result = Ok(value)
        for validator in validators:
            result = result.bind(validator)
        return result
    return composed

Konfigurierbare Pipelines

# Standard-Pipeline
process_order_standard = create_order_pipeline()

# Strikte Pipeline für B2B
process_order_strict = create_order_pipeline(
    cart_validators=[
        check_no_duplicates,
        check_max_items(5),
        check_max_quantity(10),
    ],
    min_total=50.0,
)

Keine einzige Zeile bestehender Logik wurde geändert. Open-Closed-Prinzip durch Funktionskomposition statt durch Vererbung.

8. Wo ist die Komplexität hin?

Sie wurde verlagert – aus dem fachlichen Code in die Datenstruktur:

Vorher (imperativ) Nachher (funktional)
if result is None: return None Result.bind()
try: ... except: ... Result als Rückgabetyp
if discount_code is not None: if ... apply_discount() als eigene Funktion

"Monads not only accomplish, with pure functions, what normally is done with side effects in imperative programming, but they also do it with a high degree of control and type safety." — Bartosz Milewski: Category Theory for Programmers, S. 345

Die drei Ebenen der Verlagerung

  1. Fehlerbehandlung → Result-Monade: bind statt if/else
  2. Konfiguration → Closures: Higher-Order Functions statt Parameter-Schleifen
  3. Erweiterbarkeit → Komposition: Neue Funktionen statt Code-Änderungen

Selbst ausprobieren

Eine vollständige Mini-Pipeline mit Result-Monade. Beachte: Die Geschäftslogik (parse_qty, check_stock, apply_discount) ist linear lesbar – kein if/else, kein try/except. Die gesamte Fehlerlogik lebt in bind.

from dataclasses import dataclass

@dataclass(frozen=True)
class Ok:
    value: object
    def bind(self, f): return f(self.value)
    def map(self, f):  return Ok(f(self.value))

@dataclass(frozen=True)
class Err:
    error: str
    def bind(self, f): return self
    def map(self, f):  return self

STOCK = {"A": 10, "B": 0, "C": 3}
PRICE = {"A": 5.0, "B": 2.0, "C": 12.0}

def parse_qty(raw):
    if not raw.isdigit():
        return Err(f"Menge ist keine Zahl: {raw!r}")
    return Ok(int(raw))

def check_stock(product):
    def step(qty):
        available = STOCK.get(product, 0)
        if qty > available:
            return Err(f"Nicht genug Lagerbestand für {product} ({available} < {qty})")
        return Ok(qty)
    return step

def to_total(product):
    return lambda qty: qty * PRICE[product]

def apply_discount(total):
    return total * 0.9 if total >= 50 else total

def order(product, raw_qty):
    return (
        parse_qty(raw_qty)
        .bind(check_stock(product))
        .map(to_total(product))
        .map(apply_discount)
    )

for product, qty in [("A", "4"), ("A", "20"), ("B", "1"), ("C", "abc"), ("C", "3")]:
    print(f"order({product!r}, {qty!r}) → {order(product, qty)}")

Erweiterungs-Idee: Füge einen weiteren bind-Schritt check_min_total(20.0) ein, der Bestellungen unter 20 € als Err ablehnt – ohne eine bestehende Funktion zu ändern.

Zusammenfassung

Artikel Konzept Werkzeug
1 – FP-Grundlagen Pure Functions, Immutability, Komposition lambda, map, filter, reduce
2 – Algebraische Datentypen Product Types, Sum Types, Pattern Matching @dataclass(frozen=True), match/case
3 – Funktor, Applicative, Monad Werte im Kontext transformieren und verketten Maybe, Result, Validation
4 – Komplexität verlagern Alles zusammen: Pipeline statt if/else bind, map, Komposition

Die Kernidee in einem Satz

Funktionale Programmierung verlagert Komplexität aus dem Kontrollfluss in Datenstrukturen – und Monaden sind das Werkzeug, das diese Datenstrukturen komponierbar macht.

"The word transform is the clue. Functional programming is all about transforming data. [...] This lets us build pipelines of functions." — Dave Thomas & Bruce Tate: Functional Anthology, S. 125


Quellen

Code-Beispiele

📄 Als PDF