5.14 Für Fortgeschrittene: Closures in LPC (Funktionen als Werte)
In vielen LPC-Beispielen siehst du Funktionsnamen als Strings: Man schreibt irgendwo "cmd_suchen",
und die Mudlib ruft diese Funktion später auf. Das funktioniert – aber es hat Grenzen: Strings sind nur Text.
Ein Tippfehler wird oft erst zur Laufzeit bemerkt, und du kannst schwer „Kontext“ mitgeben (also: welche Daten
sollen beim Aufruf schon fest eingebaut sein?). Genau hier kommen Closures ins Spiel.
Eine Closure ist vereinfacht gesagt ein „Funktionszeiger“ oder „Funktionsobjekt“:
Du speicherst nicht den Namen als Text, sondern eine echte Referenz auf ausführbaren Code. Das bedeutet:
Du kannst Closures in Variablen speichern, in Arrays oder Mappings ablegen, als Argument an Funktionen übergeben
und später ausführen. Damit wird LPC deutlich flexibler – vor allem für Callbacks, Filter, Sortierungen,
Ereignisse und generische Helferfunktionen.
1) Warum Closures? Ein Anfängerbild
Stell dir vor, du willst eine Liste von Objekten durchsuchen und nur die behalten, die eine bestimmte Eigenschaft
erfüllen. Du könntest hart eine Schleife schreiben, überall wiederholen, und jedes Mal die Bedingung anpassen.
Mit Closures kannst du stattdessen sagen: „Hier ist die Bedingung als Funktion – wende sie auf jedes Objekt an.“
Das ist sauberer, kürzer und wiederverwendbar.
2) Closure-Grundformen (das Wichtigste, ohne Overkill)
Je nach Driver/Mudlib gibt es unterschiedliche Schreibweisen. Im Midgard MUD sieht man häufig diese Form:
#'funktionsname. Das bedeutet: „Nimm eine Referenz auf diese Funktion.“
Viele Beispiele nutzen Closures auch indirekt, ohne dass es groß erklärt wird – zum Beispiel bei
filter(), map() oder bei Properties, die eine Closure akzeptieren.
// Beispiel: Closure auf eine Funktion in diesem Objekt
closure c = #'is_wizard;
// Später ausführen (konkret: über funcall)
if (funcall(c, this_player()))
write("Du bist ein Magier.\n");
int is_wizard(object who)
{
return who && IS_WIZARD(who);
}
Das neue Wort hier ist funcall(): Damit rufst du eine Closure auf, als würdest du eine Funktion
aufrufen. Du übergibst die Argumente genauso wie bei normalen Funktionen.
3) Closures mit filter() und map() (super praktisch)
Zwei sehr typische Helfer sind filter() und map(). Sie arbeiten auf Arrays.
filter() behält nur Elemente, für die die Bedingung wahr ist. map() wandelt jedes
Element um (z.B. Objekt → Name).
// Angenommen, du hast ein Array von Objekten:
object *inv = all_inventory(this_player());
// 1) Filter: nur lebende Wesen (living)
object *livings = filter(inv, #'living);
// 2) Map: aus Objekten Strings machen (z.B. object_name)
string *names = map(livings, #'object_name);
// Ausgabe
printf("Lebende Objekte: %O\n", names);
Wichtig: Hier siehst du ein schönes Prinzip: Du gibst nicht „den Namen einer Funktion als String“ an,
sondern die Funktion selbst als Closure. Dadurch ist vieles robuster. Wenn es die Funktion nicht gibt,
fällt es typischerweise früher auf. Und du kannst leichter zwischen Funktionen wechseln, ohne überall Strings
auszutauschen.
4) Closures mit Kontext (warum das so mächtig ist)
Eine häufige Anfängerfrage: „Wie filtere ich nach etwas, das ich zur Laufzeit kenne – z.B. nach einer ID,
einem Grenzwert oder einem Namen?“ Genau dafür sind Closures ideal, weil du den Kontext als Parameter
mitschicken kannst – entweder direkt beim Aufruf oder indem du eine Hilfsfunktion baust, die zusätzliche
Argumente akzeptiert.
// Wir filtern Inventar nach einer bestimmten ID (z.B. "flea").
// Idee: Hilfsfunktion, die (object ob, string id) prüft.
int has_id(object ob, string id)
{
return ob && ({int})ob->id(id);
}
void demo_filter_by_id(string id)
{
object *inv = all_inventory(this_player());
// filter() ruft has_id(obj, id) auf, wenn der Driver/Mudlib extra Argumente unterstützt
// (Im Midgard-Kontext ist das oft möglich. Wenn nicht, nimm eine andere Strategie, siehe Hinweis unten.)
object *hits = filter(inv, #'has_id, id);
printf("Gefunden: %O\n", map(hits, #'object_name));
}
Hinweis für Anfänger: Nicht jede Kombination ist in jeder Umgebung gleich. Manche Treiber/Mudlibs unterstützen
Zusatzargumente bei filter/map, andere erwarten nur (Array, Closure). Wenn Zusatzargumente
nicht gehen, löst du es klassisch mit einer Schleife – oder du baust die Logik anders (z.B. erst map → Daten,
dann manuell filtern). Die Grundidee bleibt: Closures helfen dir, Logik als „Baustein“ zu behandeln.
5) Objektbindung: Closure auf Funktion eines anderen Objekts
Closures können auch auf Funktionen in einem anderen Objekt zeigen. Das ist typisch bei Callbacks:
Du gibst einer Funktion „was sie später aufrufen soll“. Dadurch muss der Empfänger nicht wissen,
welche Funktion genau ausgeführt wird – er ruft einfach die Closure auf.
// Beispiel: Du willst, dass Objekt A später eine Funktion in Objekt B aufruft.
// (Pseudo-Beispiel, weil konkrete Syntax variieren kann.)
// In B:
void notify(string msg) { tell_object(this_player(), msg); }
// In A:
void do_callback(closure cb)
{
if (cb) funcall(cb, "Callback wurde ausgelöst.\n");
}
// Aufrufidee:
object b = this_object(); // nur als Beispiel
closure cb = #'notify;
do_callback(cb);
Warum ist das gut? Weil du „do_callback“ generisch hältst: Es kann jeden Callback ausführen,
solange er dieselbe Art von Argumenten akzeptiert. So baut man flexible Systeme (Events, Hooks,
Verzögerungen, Timer, UI-Reaktionen, Auslöser wie dein Hebel im Kapitel-13-Beispiel).
6) Typische Anfängerfehler
-
Strings mit Closures verwechseln:
"is_wizard" ist Text, #'is_wizard ist Code-Referenz.
Text kann Tippfehler verstecken – Closures sind meist robuster.
-
Falsche Argumente bei funcall: Wenn die Funktion zwei Parameter erwartet, musst du auch zwei übergeben.
Sonst knallt es (oder liefert falsche Ergebnisse).
-
Zu früh optimieren: Wenn du gerade erst Schleifen lernst, ist es okay, erst die Schleife zu schreiben.
Closures sind ein Werkzeug für Eleganz und Wiederverwendung – kein Muss für jede Kleinigkeit.
7) Wo du Closures im MUD sofort wiedertriffst
Du wirst Closures häufig sehen bei: filter, map, Sortierfunktionen,
Event/Hooks-Systemen, manchmal bei Properties (wenn eine Property statt Text eine Funktion zur Berechnung
akzeptiert), und bei Tools/Debugging (einige Mechaniken im MGtool arbeiten intern stark callback-orientiert).
Wenn du Closures verstanden hast, erkennst du viele „magische“ Patterns plötzlich als ganz normal:
„Aha, da wird Code als Wert weitergereicht.“
Empfehlung: Baue Closures zuerst in kleinen, ungefährlichen Stellen ein – zum Beispiel beim Filtern und Mappen von Arrays.
Sobald das sitzt, kannst du damit größere Systeme entkoppeln (z.B. Hebel löst Callback aus, Raum entscheidet, was passiert).