A ideia em uma frase
Programas Zero fazem I/O por terem recebido permissão para isso, não buscando uma variável global ambiente.
Essa permissão é um valor do tipo World. O runtime constrói um antes de chamar main, e o seu programa passa esse valor (ou pedaços dele) por onde precisar interagir com o mundo externo.
Por que sem globais?
A maioria das linguagens permite que qualquer função, em qualquer lugar, escreva no stdout ou abra um arquivo. JavaScript tem console.log. Python tem print. C tem printf. A conveniência é real, mas o custo também: você não consegue dizer pela assinatura de uma função se ela pode fazer I/O. Para saber, precisa ler o corpo — recursivamente.
Zero adota outra postura. Não há print global. Não há os.Stdout ambiente. Se a sua função faz I/O, esse fato precisa aparecer na assinatura, porque o único jeito de fazer I/O é ter recebido uma capacidade.
Os benefícios aparecem em três lugares:
- Ler uma assinatura te diz o que uma função pode fazer. Uma função que não menciona
Worldnão consegue escrever no stdout, não consegue abrir um socket, não consegue ler um arquivo. O sistema de tipos faz disso uma garantia rígida. - Testar código puro é trivial. Funções puras não precisam de stubs de
printnem de sistemas de arquivos mockados — elas nem têm acesso a essas coisas, antes de mais nada. - Agentes conseguem raciocinar localmente. Um agente de IA gerando ou reparando código Zero consegue saber — sem ler toda a base de código — se uma função que está olhando tem efeitos.
O uso canônico
Você já viu a forma básica em hello-world:
Três coisas acontecendo:
maindeclara o parâmetro comoworld: World. O runtime entrega ao programa um valorWorlde o vincula aqui.world.outé o stream de saída padrão, exposto como um campo da capacidadeWorld.world.out.write(...)escreve uma string. Ele retorna um valor falível (a escrita pode falhar), que ocheckpropaga.
Você também pode renomear o parâmetro — w: World ou io: World — mas world é a convenção e vale manter por consistência com o ecossistema Zero.
O que mora no World
A capacidade World expõe as superfícies que um programa pode precisar. A forma exata depende do que o runtime suporta, mas você pode esperar entradas para:
world.out— saída padrão.world.err— erro padrão.world.in— entrada padrão.- Uma forma de abrir arquivos, ler variáveis de ambiente e conectar pela rede.
Consulte a documentação atual da biblioteca padrão do Zero para a lista oficial de campos. Algumas superfícies (rede, sistema de arquivos) podem viver atrás de tipos de capacidade mais restritos acessíveis pelo World em vez de estar no topo.
Encanando capacidades pelo código
O detalhe — o preço que você paga pelos efeitos explícitos — é que toda função que precisa fazer I/O precisa receber a capacidade. Você não consegue buscá-la implicitamente.
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 recebe world para poder escrever por ele. Se log não recebesse world, o corpo não conseguiria chamar world.out.write — a ligação não existiria.
É mais encanamento de parâmetros do que numa linguagem com I/O ambiente. A troca é que o grafo de chamadas inteiro de main agora é visível só pelas assinaturas:
mainrecebeworld, então pode fazer I/O.logrecebeworld, então pode fazer I/O.- Qualquer função sem
worldna assinatura não pode.
Capacidades mais restritas
Passar o World inteiro para toda função é uma abordagem bruta — é tipo entregar privilégios de root. O padrão que o Zero incentiva é pegar só a fatia do World que você realmente precisa:
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")
}
Agora log só recebe acesso a um Stream (o mesmo tipo que world.out expõe). Ele consegue escrever por esse stream mas não consegue abrir um arquivo nem ler da rede. Quem chamou escolheu o que log pode fazer.
Os nomes exatos dos tipos que você vai ver em código Zero real (Stream, Writer, fatias de capacidade) vão acompanhar o vocabulário da biblioteca padrão na sua versão da toolchain. O padrão — passe o mínimo, não o máximo — é universal.
Funções puras
Funções que não precisam de World não devem receber World. Isso é em parte uma preferência de estilo e em parte imposto pelo sistema de tipos: não há jeito de fazer I/O sem uma capacidade.
fun sum(point: Point) -> i32 {
return point.x + point.y
}
sum é pura com relação ao mundo externo. Quem chamou, olhando a assinatura, sabe com certeza que essa função não vai imprimir nada, não vai abrir arquivo nenhum, não vai pingar nenhum servidor. É uma propriedade em que um analisador estático (ou um agente) pode confiar sem ler o corpo.
Capacidades e raises
Quase toda operação numa capacidade pode falhar. world.out.write pode falhar porque o stream foi fechado. Um arquivo pode falhar ao abrir porque o arquivo não existe. A superfície de API das capacidades vem em par com raises e check — operações falíveis declaram seus modos de falha na assinatura, e quem chama reconhece com check.
A combinação é o coração da história de efeitos do Zero:
- O que pode acontecer →
raises { ... }. - Por meio do quê →
World(ou uma fatia dele). - Onde → onde a capacidade e o
raisesforem visíveis.
Já é informação suficiente para raciocinar com precisão sobre os efeitos de uma função, só pela assinatura.
Uma nota sobre testes
I/O baseado em capacidades torna o teste simples por construção. Quer capturar a saída de uma função sob teste? Passe uma capacidade out falsa que registra o que foi pedido para escrever. Quer testar uma função que deveria ser pura? Não passe nenhuma capacidade — a assinatura dela não vai deixar tocar o mundo externo.
A biblioteca padrão pode fornecer arcabouços de teste que constroem capacidades falsas ou em memória exatamente para esse propósito. A API exata vai evoluir com a linguagem; o princípio (capacidades são valores que você pode substituir) é a alavanca.
A seguir: Raises e Check
World é metade da história dos efeitos — as superfícies que uma função pode tocar. A outra metade é a falha: quando algo dá errado, como ela se propaga? Isso vem a seguir em Raises e Check.
Perguntas frequentes
O que é o World em Zero?
World é o objeto de capacidade fornecido pelo runtime que dá ao programa Zero acesso ao mundo externo: stdout, stdin, arquivos, rede, variáveis de ambiente e assim por diante. O runtime constrói um valor World e passa para main. Funções que precisam fazer I/O precisam receber o World (ou uma fatia mais estreita dele) — não há atalho global.
Por que main recebe um parâmetro World?
Zero não tem globais ambiente. Não existe equivalente de printf, console.log ou os.Stdout que qualquer função possa chamar sem permissão. O runtime entrega para main uma capacidade World e main (e qualquer função que ela chamar) só consegue fazer I/O por esse valor. Isso torna cada efeito visível na assinatura da função.
Como o I/O baseado em capacidades é diferente do I/O comum?
Na maioria das linguagens, I/O é implícito — qualquer função pode escrever no stdout ou ler do sistema de arquivos a qualquer hora. O I/O baseado em capacidades transforma a permissão de fazer I/O em um valor: você precisa receber um World (ou uma fatia dele) para usar. Funções de computação pura não recebem World e, portanto, literalmente não conseguem fazer I/O — o sistema de tipos garante.
Posso obter o World implicitamente em algum lugar fundo da pilha?
Não, por desenho. Se uma função auxiliar lá no fundo da pilha precisa escrever no stdout, você precisa passar para ela o World — ou uma capacidade mais restrita — explicitamente. É mais encanamento de parâmetros do que numa linguagem com I/O ambiente, mas é o preço de conseguir ler a assinatura de uma função e saber se ela pode fazer I/O.
O que world.out.write faz?
world.out.write("texto\n") escreve a string dada no stream de saída padrão do programa, através da capacidade que o runtime forneceu. Ele retorna um valor falível — a escrita pode falhar — então você envolve a chamada com check para propagar o erro pela pilha de chamadas.