MIDGARDAudhumbla 0.7

Kapitel 5: Funktionen & Rückgabewerte

Funktionen sind das Herz von LPC. Ohne Funktionen gäbe es keine Logik, kein Verhalten und keine Interaktion. In diesem Kapitel lernst du von Grund auf, was Funktionen sind, wie sie aufgebaut sind, wie Parameter funktionieren, wie Rückgabewerte genutzt werden und wie man typische Anfängerfehler vermeidet. Wir gehen bewusst langsam vor – dieses Kapitel ist lang, aber essenziell.

Weiter zu Kapitel 6

Danach: Kontrollstrukturen – Schleifen, switch, break.

5.1 Was ist eine Funktion?

Eine Funktion ist ein benannter Codeblock, der eine bestimmte Aufgabe erfüllt. Du kannst dir eine Funktion wie eine Maschine in einer Werkstatt vorstellen: Du legst eventuell Material auf das Förderband (Parameter), die Maschine verarbeitet es (Code im Funktionskörper), und am anderen Ende kommt vielleicht ein Werkstück heraus (Rückgabewert).

Der entscheidende Vorteil von Funktionen ist Wiederverwendbarkeit: Du schreibst den Code einmal und kannst ihn beliebig oft aufrufen — mit verschiedenen Eingaben. Statt 50-mal den gleichen Block zu kopieren, schreibst du eine Funktion und rufst sie 50-mal auf. Wenn sich später die Logik ändert, musst du sie nur an einer Stelle anpassen.

Beispiele aus dem echten Leben für „Funktionen“:

  • „addiere zwei Zahlen“ — Eingabe: zwei Zahlen, Ausgabe: eine Zahl
  • „prüfe, ob ein Spieler lebt“ — Eingabe: ein Objekt, Ausgabe: 0 oder 1
  • „gib mir die Beschreibung dieses Items“ — Eingabe: nichts, Ausgabe: ein String
  • „heile diesen Spieler um 10 LP“ — Eingabe: Spieler, Ausgabe: nichts (nur Effekt)
  • „finde alle Spieler im Raum“ — Eingabe: Raum, Ausgabe: Array von Objekten

In LPC ist fast alles eine Funktion: Ausgaben (write()), Abfragen (QueryProp()), Aktionen (move()), Berechnungen, Steuerung, Ereignisbehandlung. Selbst create() ist eine Funktion — der Driver ruft sie auf, wenn dein Objekt entsteht.

Im Midgard MUD unterscheidet man drei Arten von Funktionen, die du oft hörst:

  • Lfuns (Local Functions) — die du in deinen LPC-Dateien selbst schreibst
  • Efuns (External Functions) — vom Driver bereitgestellte Funktionen wie write(), find_player(), random()
  • Sefuns (Simulated Efuns) — Funktionen, die wie Efuns aufgerufen werden, aber in der Mudlib in LPC implementiert sind, z.B. break_string()

Je besser du Funktionen verstehst, desto sauberer wird dein Code. Funktionen sind die Bausteine — Objekte sind nur Container für Funktionen und Variablen.

5.2 Der grundlegende Aufbau einer Funktion

Jede Funktion besteht aus vier Teilen, die in einer festen Reihenfolge stehen:

  1. Rückgabewert-Typ — was die Funktion herausgibt (oder void, falls nichts)
  2. Name der Funktion — wie du sie aufrufst
  3. Parameterliste — was sie hineinbekommt (kann leer sein)
  4. Funktionskörper — der eigentliche Code, in geschweiften Klammern

int add(int a, int b)   // Typ + Name + Parameter
{                       // Körper Anfang
  return a + b;         // tut etwas und gibt das Ergebnis zurück
}                       // Körper Ende
      

Das liest sich so: „Die Funktion add bekommt zwei int-Werte (einen a und einen b) und gibt einen int zurück — nämlich die Summe von a und b.“

Mit Modifier davor

Im Midgard-Code wirst du oft Modifier vor dem Rückgabetyp sehen, die regeln, wer die Funktion aufrufen darf:


public int query_hp() { return hp; }
private void internal_helper() { /* ... */ }
protected void create() { ::create(); }
varargs int log(string text, string tag) { /* ... */ }
      

Funktionsdeklaration vs. Funktionsdefinition

Manchmal will man die Funktion nur ankündigen, ohne sie sofort zu schreiben — etwa, wenn zwei Funktionen sich gegenseitig rufen. Dann nutzt du eine Deklaration (mit Strichpunkt am Ende, ohne Körper):


// Deklaration (Prototyp):
int berechne_schaden(object enemy);

// ... irgendwo später ...

// Definition (mit Körper):
int berechne_schaden(object enemy) {
  return 10 + enemy->QueryProp(P_LEVEL);
}
      

Bei der Mudlib gibt es viele Funktionen aus geerbten Klassen, von denen du meistens nur die Definition in /std/... findest, aber im eigenen Code direkt anrufst — auch das funktioniert dank Prototypen.

5.3 Rückgabewerte – was kommt aus einer Funktion heraus?

Der Rückgabewert ist das Ergebnis einer Funktion. Sein Typ steht ganz vorne in der Funktionsdefinition. Mit dem Schlüsselwort return beendest du die Funktion und übergibst dem Aufrufer das Ergebnis. Du kannst eine Funktion auch früher mit return verlassen — z.B. wenn eine Vorbedingung nicht erfüllt ist.


int get_level()
{
  return 10;
}
      

Die Funktion gibt immer einen int zurück. Das Schlüsselwort return beendet die Funktion sofort. Alle Anweisungen nach dem return werden nicht mehr ausgeführt.

Rückgabewerte benutzen

Du kannst Rückgabewerte direkt einer Variable zuweisen oder in einem Ausdruck verwenden:


int lvl;

lvl = get_level();           // Wert in Variable speichern

if (get_level() > 5)         // direkt in einer Bedingung verwenden
  write("Stark!\n");

int doppelt = get_level() * 2;   // in einer Berechnung verwenden

write("Level " + get_level() + "\n");  // in String-Konkatenation
      

Mehrere returns — frühe Ausstiege

Es ist absolut normal und sogar empfohlen, eine Funktion früh zu verlassen, sobald klar ist, dass die weitere Logik nicht mehr nötig ist. Das hält Funktionen flach und lesbar (manche nennen es „Guard Clauses“):


int can_enter(object pl)
{
  if (!pl)        return 0;     // kein Spieler? raus.
  if (!living(pl))return 0;     // kein Lebewesen? raus.
  if (pl->QueryProp(P_LEVEL) < 10) return 0;  // zu schwach? raus.

  // wenn wir hier ankommen, sind alle Bedingungen erfüllt:
  return 1;
}
      

Was du nicht machen solltest: einen Wert zurückgeben, der nicht zum deklarierten Typ passt. Wenn deine Funktion int liefern soll, gib auch wirklich einen int zurück — nicht 0, wenn du eigentlich ein Objekt versprochen hast (auch wenn 0 für Objekte „leer“ heißt — das funktioniert nur, weil Objekt-Variablen 0 sein dürfen).

5.4 void-Funktionen – wenn nichts zurückkommt

Manche Funktionen sollen nichts zurückgeben, sondern nur etwas tun: Text ausgeben, einen Zustand ändern, ein anderes Objekt benachrichtigen, einen Effekt auslösen. Bei solchen Funktionen ist es ehrlicher, dem Compiler und allen Lesenden zu sagen: „Hier kommt nichts zurück.“ Genau das tut der Rückgabetyp void.


void say_hello()
{
  write("Hallo!\n");
}

void heal_player(object pl, int amount)
{
  if (!pl) return;          // Frühausstieg: nichts zu tun
  pl->buffer_hp(amount, 5); // val + rate (HP/Tick) — beide Pflicht
  tell_object(pl, "Du fühlst dich besser.\n");
}
      

Beachte: return; ohne Wert ist in void-Funktionen erlaubt und beendet die Funktion vorzeitig. return wert; mit Wert ist hingegen in void-Funktionen verboten — das wäre widersprüchlich.

Was du nicht darfst

Du darfst das Ergebnis einer void-Funktion nicht speichern oder verwenden — sie hat ja keins:


// falsch!
int x = say_hello();           // FEHLER: void hat keinen Wert

// auch falsch:
if (say_hello())               // FEHLER: kein bool-Ergebnis
  write("...");

// richtig: einfach aufrufen:
say_hello();
      

Wann sollte man void nehmen, wann int?

Faustregel: Wenn deine Funktion etwas tut und man wissen will, ob es geklappt hat, nimm int als Rückgabewert (1 = erfolgreich, 0 = nicht). Wenn deine Funktion etwas tut und der Erfolg „immer“ klar ist (oder egal), nimm void:


// Aktion mit Erfolgsrueckmeldung:
int try_attack(object enemy) {
  if (!enemy) return 0;
  if (!living(enemy)) return 0;
  enemy->Defend(10, ({DT_BLUDGEON}), 0, this_object());
  return 1;
}

// Reine Aktion:
void announce(string text) {
  tell_room(environment(this_object()), text);
}
      

Im Midgard MUD-Code wirst du void sehr häufig sehen: alle Setter (set_hp, SetProp), alle create()-Funktionen, alle Hook-Callbacks ohne Rückgabewert, viele Aktion-Funktionen.

5.5 Parameter – Werte an Funktionen übergeben

Parameter sind Eingabewerte, die du beim Aufruf einer Funktion mitgibst. Sie stehen in den runden Klammern hinter dem Funktionsnamen. Innerhalb der Funktion sind die Parameter wie ganz normale lokale Variablen — sie existieren nur während der Funktionsausführung.

Der Trick an Parametern: Sie machen Funktionen universell. Statt eine Funktion zu schreiben, die immer „die Zahl 5 quadriert“, schreibst du eine Funktion, die „eine beliebige Zahl quadriert“ — und übergibst beim Aufruf die Zahl, die dich gerade interessiert.


int square(int x)        // x ist der Parameter
{
  return x * x;          // x als lokale Variable benutzbar
}
      

Aufruf:


int result;

result = square(5);   // result = 25
result = square(7);   // result = 49

// Auch direkt mit Variable:
int n = 10;
result = square(n);   // result = 100

// Oder mit Ausdruck:
result = square(n + 2);   // result = 144 (12 * 12)
      

Parameter sind unabhängig vom Original (Pass-by-Value)

Wenn du einen int oder string als Parameter übergibst, bekommt die Funktion eine Kopie. Veränderungen am Parameter wirken sich nicht auf die Originalvariable aus:


void verdoppeln(int x) {
  x = x * 2;             // Kopie wird verändert
  write("In der Funktion: " + x + "\n");
}

void demo() {
  int n = 5;
  verdoppeln(n);
  write("Nach dem Aufruf: " + n + "\n");
}
// Ausgabe:
// In der Funktion: 10
// Nach dem Aufruf: 5
      

Bei Arrays, Mappings und Objekten ist das anders: Die werden als Referenz weitergegeben. Veränderungen wirken sich also auch außerhalb der Funktion aus (siehe Kapitel 7 für die „Referenz-Falle“ bei Arrays/Mappings).

Parameter sind lokale Variablen innerhalb der Funktion. Du kannst sie wie jede andere lokale Variable behandeln: ändern, in Ausdrücken verwenden, an andere Funktionen weitergeben. Sie existieren nur während der Funktionsausführung und verschwinden danach.

5.6 Mehrere Parameter

Funktionen können beliebig viele Parameter haben. Jeder Parameter hat einen eigenen Datentyp und Namen, und sie werden durch Komma getrennt. Die Reihenfolge ist wichtig — beim Aufruf musst du die Argumente in genau dieser Reihenfolge übergeben.


string full_name(string first, string last)
{
  return first + " " + last;
}
      

string name;

name = full_name("Odin", "Allvater");   // → "Odin Allvater"
name = full_name("Allvater", "Odin");   // → "Allvater Odin"  (Reihenfolge zählt!)
      

Verschiedene Typen mischen

Parameter dürfen unterschiedliche Typen haben — du musst nur jeden korrekt deklarieren:


// Spielerobjekt, Schadenshöhe, Schadenstyp:
int do_damage(object target, int amount, string dam_type) {
  if (!target) return 0;
  target->Defend(amount, ({dam_type}), 0, this_object());
  return amount;
}

// Aufruf:
int dealt = do_damage(this_player(), 25, DT_FIRE);
      

Wie viele Parameter sind sinnvoll?

Faustregel: Mehr als 4–5 Parameter werden unübersichtlich. Wenn du merkst, dass deine Funktion 7 oder 10 Parameter hat, sammle zusammengehörige Werte in einem Mapping oder Struct, oder bilde mehrere kleinere Funktionen.


// Schwer zu lesen:
make_npc("Hrafn", "Wikinger", 12, 100, 80, 5, MALE, 0, 1);

// Besser:
mapping config = ([
  "name":   "Hrafn",
  "race":   "Wikinger",
  "level":  12,
  "hp":     100,
  "wc":     80,
  "ac":     5,
  "gender": MALE,
]);
make_npc(config);
      

5.7 Funktionen auf Objekten aufrufen

LPC ist objektorientiert — das heißt, Funktionen leben in Objekten und werden meistens auf einem konkreten Objekt aufgerufen. Du sagst nicht „rufe die Funktion Name“, sondern „rufe Name(WER) auf diesem Spieler“. Dafür gibt es den Pfeil-Operator ->.


object pl;

pl = this_player();
write(pl->Name(WER) + "\n");
      

Der Operator -> bedeutet: „Rufe diese Funktion auf diesem Objekt auf.“ Im Hintergrund ist das ein Aufruf der Efun call_other(objekt, "funktionsname", args...) — der Pfeil ist nur die kurze Schreibweise. Du wirst beide Varianten in fremdem Code sehen:


// Diese drei Aufrufe sind äquivalent:
pl->Name(WER);
call_other(pl, "Name", WER);
pl.Name(WER);          // neuere Syntax mit Punkt
      

Funktionen im eigenen Objekt

Wenn du eine Funktion im eigenen Objekt aufrufst, brauchst du keinen Pfeil. Du schreibst einfach den Funktionsnamen:


public int query_hp() { return hp; }

public void status() {
  // direkter Aufruf, weil im selben Objekt:
  write("Du hast " + query_hp() + " LP.\n");
}
      

Vermeide this_object()->funktion() aus Faulheit — das ist langsamer (call_other ist teurer als ein direkter Aufruf) und liest sich auch nicht besser.

Sehr wichtig: Prüfe immer, ob das Objekt existiert!

Bevor du eine Funktion auf einem fremden Objekt aufrufst, solltest du sicherstellen, dass das Objekt überhaupt da ist. Andernfalls bekommst du einen Runtime-Error:


// Unsicher:
write(pl->Name(WER));        // Crash, wenn pl == 0

// Sicher:
if (pl)
  write(pl->Name(WER));

// Oder kompakter (Short-Circuit):
if (pl && pl->QueryProp(P_LEVEL) >= 10)
  write("Du bist erfahren genug.\n");
      

Im Midgard sind viele Funktionen geschützt: Sie geben einfach 0 zurück, wenn der Aufruf nicht erlaubt ist (z.B. weil das aufrufende Objekt zu wenig Rechte hat). Verlass dich nicht darauf, dass ein Aufruf immer das tut, was du erwartest. Defensives Programmieren ist Pflicht — siehe Kapitel 11.

5.8 Frühzeitiges Verlassen einer Funktion

Mit return kannst du eine Funktion jederzeit verlassen. Das wird oft für Prüfungen genutzt.


int can_enter(object pl)
{
  if (!pl)
    return 0;

  if (pl->QueryProp(P_LEVEL) < 10)
    return 0;

  return 1;
}
      

Diese Struktur ist sehr typisch im Midgard MUD.

5.9 Sichtbarkeit von Funktionen

Wie bei Variablen gibt es auch bei Funktionen Sichtbarkeiten.


public int query_hp();
protected void heal();
private void internal_reset();
      
  • public – jeder darf die Funktion aufrufen
  • protected – nur dieses Objekt und Erben
  • private – nur dieses Objekt selbst

Anfänger-Tipp: Mache nur das public, was wirklich von außen gebraucht wird.

5.9a varargs – Funktionen mit variabler Argumentzahl

Viele Lib-Funktionen im Midgard MUD akzeptieren optional weitere Argumente. Das macht man mit dem Schlüsselwort varargs:


// Die letzten Parameter sind optional und werden dann mit 0 belegt.
varargs void greet(string name, string title) {
  if (!title) title = "Reisende(r)";
  write(title + " " + name + ", sei willkommen!\n");
}

greet("Odin");                // -> "Reisende(r) Odin, sei willkommen!"
greet("Odin", "Allvater");    // -> "Allvater Odin, sei willkommen!"
      

Wichtig: varargs kommt vor dem Rueckgabetyp. Korrekte Reihenfolge:


[ modifier ] [ varargs ] [ return-type ] funktion(parameter)
// Beispiele:
public  varargs int  query_hp(int extra);
private varargs void log(string msg, string tag);
      

Sammel-Variante: alle Restargumente in ein Array

Wenn der letzte Parameter selbst varargs deklariert ist, sammelt er alle übrigen Argumente in einem Array. Das ist die LDMud-Variante von „Rest-Args“:


varargs void log_all(string tag, varargs string * msgs) {
  foreach(string m : msgs)
    write("[" + tag + "] " + m + "\n");
}

log_all("DEBUG", "Hallo", "Welt", "test");
      

5.9b Modifier-Spickzettel

Vor Funktionen können mehrere Modifier stehen. Die wichtigsten:

  • public – aufrufbar von überall (Default)
  • protected – aufrufbar nur intern und von erbenden Objekten (NICHT per ->)
  • private – nur in genau diesem Objekt
  • static – alter Begriff für „nicht per call_other()“; verwende lieber protected
  • nomask – Funktion kann von erbenden Objekten nicht überschrieben werden
  • varargs – siehe oben
  • deprecated – Compiler warnt bei Aufruf

Faustregel im Midgard: So restriktiv wie möglich. public ist nicht Standard-„weil-bequem“, sondern eine bewusste API-Entscheidung.

5.9c Inline-Closures – die kurze Schwester von lambda()

Inline-Closures sind der idiomatische Weg, kleine Funktionen direkt vor Ort zu schreiben. Die Argumente sind $1, $2, … oder du gibst Typen explizit an.


// Kurze Form: Ausdruck
object *livings = filter(all_inventory(this_player()),
                         (: living($1) :));

// Mit Statements
object *kleine = filter(all_inventory(this_player()),
                        (: return $1->QueryProp(P_WEIGHT) < 500; :));

// Mit Typen und Kontextvariablen
int    schwelle = 100;
object *teure   = filter(all_inventory(this_player()),
                          function int (object o) : int s = schwelle
                          { return o->QueryProp(P_VALUE) > s; });
      

Inline-Closures sind viel lesbarer als lambda() und werden im Midgard MUD klar bevorzugt. Mehr dazu unter /doc/LPC/inline-closures.

5.10 Häufige Anfängerfehler bei Funktionen

  • Falscher Rückgabewert-Typ
  • return vergessen
  • Parameter in falscher Reihenfolge
  • Objekte nicht auf 0 prüfen
  • Zu große Funktionen schreiben
  • ::create() in einem überschriebenen create() vergessen
  • varargs falsch positioniert (immer vor dem Rueckgabetyp!)

// falsch: kein return
int broken()
{
  int x = 5;
}
      

5.11 Gute Funktionsnamen

Funktionsnamen sind extrem wichtig für Lesbarkeit.

  • query_hp()
  • set_name(string n)
  • doit()
  • f1()

Gute Namen sparen Erklärungen.

5.12 Mini-Übungen

  1. Schreibe eine Funktion int is_even(int x).
  2. Schreibe eine Funktion, die einen Namen zurückgibt.
  3. Schreibe eine Funktion, die prüft, ob ein Spieler existiert.

int is_even(int x)
{
  if (x % 2 == 0)
    return 1;
  return 0;
}
      

5.13 Zusammenfassung

  • Funktionen kapseln Logik
  • Parameter sind Eingaben
  • Rückgabewerte sind Ergebnisse
  • return beendet Funktionen
  • Sichtbarkeit schützt vor Fehlern

Wenn du dieses Kapitel verstanden hast, kannst du aktiv Verhalten programmieren.

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

Ausblick

In Kapitel 6 lernst du Kontrollstrukturen: Schleifen (for, while), switch, break und typische Muster im Midgard MUD.

Weiter zu Kapitel 6

Kontrolle über den Programmfluss.