La idea en una frase
Los programas de Zero hacen E/S porque se les da permiso para hacerlo, no porque echen mano de un global ambiental.
Ese permiso es un valor de tipo World. El runtime construye uno antes de llamar a main, y tu programa lo va pasando (o partes de él) allá donde necesite interactuar con el mundo exterior.
¿Por qué no globales?
La mayoría de lenguajes permiten que cualquier función, en cualquier sitio, escriba en stdout o abra un archivo. JavaScript tiene console.log. Python tiene print. C tiene printf. La comodidad es real, pero también el coste: no puedes saber por la firma de una función si podría hacer E/S. Para saberlo, tienes que leer el cuerpo, recursivamente.
Zero adopta una postura distinta. No existe un print global. No existe un os.Stdout ambiental. Si tu función hace E/S, ese hecho tiene que aparecer en su firma, porque la única forma de hacer E/S es haber recibido una capacidad.
Los beneficios aparecen en tres sitios:
- Leer una firma te dice lo que la función puede hacer. Una función que no menciona
Worldno puede escribir en stdout, no puede abrir un socket, no puede leer un archivo. El sistema de tipos lo garantiza. - Testear código puro es trivial. Las funciones puras no necesitan
printstub ni sistemas de archivos mockeados: directamente no tienen acceso a ellos. - Los agentes pueden razonar localmente. Un agente de IA que genera o repara código de Zero puede saber — sin leer toda la base de código — si una función que está mirando tiene efectos.
El uso canónico
Ya viste la forma básica en hola mundo:
Tres cosas pasando:
maindeclara su parámetro comoworld: World. El runtime entrega al programa un valorWorldy lo vincula aquí.world.outes el flujo de salida estándar, expuesto como un campo de la capacidadWorld.world.out.write(...)escribe una cadena. Devuelve un valor falible (la escritura podría fallar), quecheckpropaga.
Puedes renombrar el parámetro — w: World o io: World — pero world es la convención y conviene mantenerla por coherencia con el ecosistema más amplio de Zero.
Qué vive en World
La capacidad World expone las superficies que un programa pudiera necesitar. La forma exacta depende de lo que soporte el runtime, pero puedes esperar entradas para:
world.out— salida estándar.world.err— error estándar.world.in— entrada estándar.- Una forma de abrir archivos, leer variables de entorno y conectar por red.
Consulta la documentación actual de la biblioteca estándar de Zero para la lista autoritativa de campos. Algunas superficies (red, sistema de archivos) pueden vivir tras tipos de capacidad más estrechos accesibles a través de World en lugar de en el nivel superior.
Pasar capacidades por el código
La pega — el precio que pagas por los efectos explícitos — es que cualquier función que necesite hacer E/S tiene que recibir la capacidad. No puedes echar mano de ella implícitamente.
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 recibe world para poder escribir a través de él. Si log no recibiera world, el cuerpo no podría llamar a world.out.write — la ligadura no existiría.
Esto es más cableado de parámetros que en un lenguaje con E/S ambiental. A cambio, todo el grafo de llamadas de main se ve solo desde las firmas:
mainrecibeworld, así que podría hacer E/S.logrecibeworld, así que podría hacer E/S.- Cualquier función sin
worlden su firma no puede.
Capacidades más estrechas
Pasar todo el World a cada función es un enfoque burdo: es como repartir privilegios de root. El patrón que Zero fomenta es recibir solo la porción de World que de verdad necesitas:
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")
}
Ahora log solo recibe acceso a un Stream (el mismo tipo que expone world.out). Puede escribir a través de él pero no puede abrir un archivo ni leer de la red. Quien llama decidió qué se le permite hacer a log.
Los nombres de tipo exactos que verás en código real de Zero (Stream, Writer, porciones de capacidad) seguirán el vocabulario de la biblioteca estándar en tu versión del toolchain. El patrón — pasa el mínimo, no el máximo — es universal.
Funciones puras
Las funciones que no necesitan World no deberían recibir World. Es en parte una preferencia de estilo y en parte algo que el sistema de tipos garantiza: no hay forma de hacer E/S sin una capacidad.
fun sum(point: Point) -> i32 {
return point.x + point.y
}
sum es puro respecto al mundo exterior. Quien llama, al mirar la firma, sabe con certeza que esta función no va a imprimir nada, no va a abrir un archivo y no va a hacer ping a un servidor. Esa es una propiedad en la que un analizador estático (o un agente) puede confiar sin leer el cuerpo.
Capacidades y raises
Casi todas las operaciones sobre una capacidad son falibles. world.out.write puede fallar porque el flujo esté cerrado. La apertura de un archivo puede fallar porque el archivo no existe. La superficie de la API de capacidades va emparejada con raises y check: las operaciones falibles declaran sus modos de fallo en sus firmas y quienes las invocan los reconocen con check.
La combinación es el corazón de la historia de efectos de Zero:
- Qué puede pasar →
raises { ... }. - A través de qué →
World(o una porción suya). - Dónde → allá donde la capacidad y
raisessean visibles.
Eso es suficiente información para razonar con precisión sobre los efectos de una función solo desde su firma.
Una nota sobre testing
La E/S basada en capacidades hace el testing simple por construcción. ¿Quieres capturar la salida de una función bajo test? Pásale una capacidad out falsa que registre qué se le pidió escribir. ¿Quieres testear una función que se supone que es pura? No le pases ninguna capacidad — su firma no le permitirá tocar el mundo exterior.
La biblioteca estándar puede proveer arneses de test que construyan capacidades falsas o en memoria justamente para esto. La API exacta evolucionará con el lenguaje; el principio (las capacidades son valores que puedes sustituir) es la palanca.
Lo siguiente: raises y check
World es la mitad de la historia de efectos: las superficies que una función puede tocar. La otra mitad son los fallos: cuando algo sale mal, ¿cómo se propaga? Eso lo cubre la próxima página, Raises y Check.
Preguntas frecuentes
¿Qué es World en Zero?
World es el objeto de capacidad proporcionado por el runtime que da a un programa de Zero acceso al mundo exterior: stdout, stdin, archivos, red, variables de entorno, etc. El runtime construye un valor World y se lo pasa a main. Las funciones que necesitan hacer E/S tienen que recibir el World (o una porción más estrecha de él): no hay escotilla de escape global.
¿Por qué main recibe un parámetro World?
Zero no tiene globales ambientales. No hay un equivalente de printf, console.log u os.Stdout que cualquier función pueda llamar sin permiso. El runtime entrega a main una capacidad World y main (y cualquier función a la que llame) solo puede hacer E/S a través de ese valor. Eso hace visible cada efecto en la firma de la función.
¿En qué se diferencia la E/S basada en capacidades de la E/S normal?
En la mayoría de lenguajes, la E/S es implícita: cualquier función puede escribir en stdout o leer del sistema de archivos en cualquier momento. La E/S basada en capacidades convierte el permiso para hacer E/S en un valor: tienes que recibir un World (o una porción suya) para usarlo. Las funciones de cómputo puro no reciben World y por tanto literalmente no pueden hacer E/S, algo que el sistema de tipos garantiza.
¿Puedo conseguir el World implícitamente en lo profundo de mi pila de llamadas?
No, por diseño. Si un helper profundo en la pila necesita escribir en stdout, tienes que pasarle el World — o una capacidad más estrecha — explícitamente. Eso es más cableado de parámetros que en un lenguaje con E/S ambiental, pero es el coste de poder leer la firma de una función y saber si puede hacer E/S.
¿Qué hace world.out.write?
world.out.write("texto\n") escribe la cadena dada en el flujo de salida estándar del programa a través de la capacidad que el runtime ha proporcionado. Devuelve un valor falible — la escritura podría fallar — así que envuelves la llamada con check para propagar el error hacia arriba.