Menu

Die Zero World-Capability: Explizite I/O ohne Globals

Zero hat kein globales stdout, keinen ambient verfügbaren Dateisystemzugriff, kein implizites Netzwerk. Alles, was die Außenwelt berührt, läuft über eine World-Capability, die an main übergeben wird. Hier ist das Warum und Wie.

Diese Seite enthält ausführbare Editoren — bearbeiten, ausführen und Ausgabe sofort sehen.

Die Idee in einem Satz

Zero-Programme machen I/O, indem ihnen die Erlaubnis dazu gegeben wird, nicht indem sie zu einer ambient Globals greifen.

Diese Erlaubnis ist ein Wert vom Typ World. Die Runtime konstruiert einen, bevor sie main aufruft, und dein Programm fädelt ihn (oder Teile davon) überall dort durch, wo es mit der Außenwelt interagieren muss.

Warum keine Globals?

Die meisten Sprachen erlauben jeder Funktion überall, auf stdout zu schreiben oder eine Datei zu öffnen. JavaScript hat console.log. Python hat print. C hat printf. Die Bequemlichkeit ist real, aber die Kosten sind es auch: Du kannst der Signatur einer Funktion nicht ansehen, ob sie I/O macht. Um es zu wissen, musst du den Body lesen – rekursiv.

Zero nimmt eine andere Haltung ein. Es gibt kein globales print. Es gibt kein ambient verfügbares os.Stdout. Wenn deine Funktion I/O macht, muss diese Tatsache in ihrer Signatur auftauchen, weil der einzige Weg zur I/O über eine erhaltene Capability führt.

Der Nutzen zeigt sich an drei Stellen:

  1. Eine Signatur zu lesen sagt dir, was eine Funktion tun kann. Eine Funktion, die World nicht erwähnt, kann nicht auf stdout schreiben, keinen Socket öffnen, keine Datei lesen. Das Typsystem macht das zur harten Garantie.
  2. Reinen Code zu testen ist trivial. Reine Funktionen brauchen keine Stub-prints oder gemockten Dateisysteme – sie haben darauf gar keinen Zugriff.
  3. Agenten können lokal reasonieren. Ein KI-Agent, der Zero-Code generiert oder repariert, kann – ohne die ganze Codebasis zu lesen – wissen, ob eine Funktion, die er anschaut, Effekte hat.

Der kanonische Einsatz

Die grundlegende Form hast du schon aus hello-world gesehen:

Drei Dinge passieren:

  • main deklariert seinen Parameter als world: World. Die Runtime reicht dem Programm einen World-Wert und bindet ihn hier.
  • world.out ist der Standardausgabe-Stream, freigegeben als Feld der World-Capability.
  • world.out.write(...) schreibt einen String. Es liefert einen fehlbaren Wert zurück (der Schreibvorgang könnte fehlschlagen), den check propagiert.

Du kannst den Parameter auch umbenennen – w: World oder io: World – aber world ist die Konvention und es lohnt sich, sie für Konsistenz mit dem breiteren Zero-Ökosystem beizubehalten.

Was auf World lebt

Die World-Capability stellt die Oberflächen bereit, die ein Programm brauchen könnte. Die genaue Form hängt davon ab, was die Runtime unterstützt, aber du kannst Einträge erwarten für:

  • world.out – Standardausgabe.
  • world.err – Standardfehlerausgabe.
  • world.in – Standardeingabe.
  • Eine Möglichkeit, Dateien zu öffnen, Umgebungsvariablen zu lesen und übers Netzwerk zu verbinden.

Schau in die aktuelle Doku der Zero-Standardbibliothek nach der maßgeblichen Liste der Felder. Einige Oberflächen (Netzwerk, Dateisystem) leben eventuell hinter engeren Capability-Typen, die über World zugänglich sind, statt auf der obersten Ebene.

Capabilities durch den Code fädeln

Der Haken – der Preis, den du für explizite Effekte zahlst – ist, dass jede Funktion, die I/O machen muss, die Capability bekommen muss. Implizit kannst du nicht zu ihr greifen.

fun log(world: World, message: String) -> Void raises {
    check world.out.write(message)
}

pub fun main(world: World) -> Void raises {
    log(world, "starting\n")
    log(world, "done\n")
}

log nimmt world, damit es darüber schreiben kann. Würde log world nicht nehmen, könnte der Body world.out.write nicht aufrufen – das Binding würde nicht existieren.

Das ist mehr Parameter-Durchreichen als in einer Sprache mit ambient I/O. Der Tausch: Der gesamte Aufrufgraph für main ist jetzt allein aus den Signaturen sichtbar:

  • main nimmt world, also könnte es I/O machen.
  • log nimmt world, also könnte es I/O machen.
  • Jede Funktion ohne world in ihrer Signatur kann es nicht.

Engere Capabilities

Die ganze World an jede Funktion zu übergeben ist ein grobes Vorgehen – als würdest du Root-Rechte verteilen. Das Muster, das Zero ermutigt, ist, nur den Ausschnitt von World zu nehmen, den du tatsächlich brauchst:

fun log(out: Stream, message: String) -> Void raises {
    check out.write(message)
}

pub fun main(world: World) -> Void raises {
    log(world.out, "starting\n")
    log(world.out, "done\n")
}

Jetzt bekommt log nur Zugriff auf einen Stream (denselben Typ, den world.out freigibt). Es kann darüber schreiben, aber keine Datei öffnen oder vom Netzwerk lesen. Der Aufrufer hat entschieden, was log tun darf.

Die genauen Typnamen, die du in echtem Zero-Code siehst (Stream, Writer, Capability-Slices), folgen dem Vokabular der Standardbibliothek deiner Toolchain-Version. Das Muster – das Minimum übergeben, nicht das Maximum – ist universell.

Reine Funktionen

Funktionen, die World nicht brauchen, sollten World nicht nehmen. Das ist teils Stilpräferenz, teils vom Typsystem erzwungen: Ohne Capability gibt es keinen Weg zu I/O.

fun sum(point: Point) -> i32 {
    return point.x + point.y
}

sum ist rein im Hinblick auf die Außenwelt. Ein Aufrufer, der die Signatur ansieht, weiß mit Sicherheit, dass diese Funktion nichts drucken, keine Datei öffnen und keinen Server anpingen wird. Das ist eine Eigenschaft, auf die sich ein statischer Analyzer (oder ein Agent) verlassen kann, ohne den Body zu lesen.

Capabilities und raises

Fast jede Operation auf einer Capability ist fehlbar. world.out.write kann fehlschlagen, weil der Stream geschlossen ist. Eine Datei zu öffnen kann fehlschlagen, weil die Datei nicht existiert. Die Capability-API-Oberfläche ist mit raises und check gepaart – fehlbare Operationen deklarieren ihre Fehlerarten in ihren Signaturen, und Aufrufer bestätigen sie mit check.

Die Kombination ist das Herz von Zeros Effekt-Geschichte:

  • Was kann passieren → raises { ... }.
  • WodurchWorld (oder ein Ausschnitt).
  • Wo → überall dort, wo die Capability und raises sichtbar sind.

Das sind genug Informationen, um präzise über die Effekte einer Funktion allein anhand ihrer Signatur nachzudenken.

Eine Anmerkung zum Testen

Capability-basierte I/O macht Testen von Natur aus einfach. Willst du die Ausgabe einer zu testenden Funktion einfangen? Übergib ihr eine Fake-out-Capability, die mitschreibt, was sie schreiben sollte. Willst du eine Funktion testen, die rein sein soll? Übergib ihr keine Capability – ihre Typsignatur lässt sie die Außenwelt nicht anfassen.

Die Standardbibliothek kann Test-Harnesses anbieten, die genau zu diesem Zweck Fake- oder In-Memory-Capabilities bauen. Die genaue API wird mit der Sprache reifen; das Prinzip (Capabilities sind Werte, die du ersetzen kannst) ist der Hebel.

Als Nächstes: Raises und Check

World ist die Hälfte der Effekt-Geschichte – die Oberflächen, die eine Funktion anfassen kann. Die andere Hälfte sind Fehler: Wenn etwas schiefgeht, wie propagiert es sich? Das deckt als Nächstes Raises und Check ab.

Häufig gestellte Fragen

Was ist die World in Zero?

World ist das von der Runtime bereitgestellte Capability-Objekt, das einem Zero-Programm Zugriff auf die Außenwelt gewährt: stdout, stdin, Dateien, das Netzwerk, Umgebungsvariablen und so weiter. Die Runtime konstruiert einen World-Wert und übergibt ihn an main. Funktionen, die I/O machen müssen, müssen die World (oder einen engeren Ausschnitt davon) bekommen – es gibt keinen globalen Ausweg.

Warum nimmt main einen World-Parameter?

Zero hat keine ambient Globals. Es gibt kein Äquivalent zu printf, console.log oder os.Stdout, das jede Funktion ohne Erlaubnis aufrufen kann. Die Runtime reicht main eine World-Capability, und main (und jede von ihr aufgerufene Funktion) kann nur durch diesen Wert I/O machen. Dadurch wird jeder Effekt in der Signatur einer Funktion sichtbar.

Wie unterscheidet sich Capability-basierte I/O von normaler I/O?

In den meisten Sprachen ist I/O implizit – jede Funktion kann jederzeit auf stdout schreiben oder vom Dateisystem lesen. Capability-basierte I/O macht die Erlaubnis zur I/O zu einem Wert: du musst eine World (oder einen Ausschnitt davon) bekommen, um sie zu nutzen. Reine Rechen-Funktionen nehmen kein World und können darum buchstäblich keine I/O machen, was das Typsystem erzwingt.

Kann ich die World irgendwo tief im Aufrufstack implizit bekommen?

Nein, by design. Wenn ein Helfer tief im Aufrufstack auf stdout schreiben muss, musst du ihm die World – oder eine engere Capability – explizit übergeben. Das ist mehr Parameter-Durchreichen als in einer Sprache mit ambient I/O, aber es ist der Preis dafür, dass man der Signatur einer Funktion ansehen kann, ob sie I/O machen könnte.

Was macht world.out.write?

world.out.write("text\n") schreibt den gegebenen String in den Standardausgabe-Stream des Programms über die Capability, die die Runtime bereitgestellt hat. Es liefert einen fehlbaren Wert zurück – der Schreibvorgang könnte fehlschlagen – also umschließt du den Aufruf mit check, um den Fehler den Aufrufstack hochzupropagieren.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S