7 pontos por GN⁺ 2025-08-22 | 1 comentários | Compartilhar no WhatsApp
  • O Zig se baseia em uma sintaxe com chaves semelhante à do Rust, mas a melhora com uma semântica de linguagem mais simples e escolhas sintáticas mais refinadas
  • Literais inteiros começam todos com o tipo comptime_int e são convertidos explicitamente na atribuição, enquanto literais de string usam uma notação concisa de string bruta baseada em \\
  • Literais de registro no formato .x = 1 facilitam a busca por escritas em campos, e todos os tipos são expressos de forma consistente com notação de prefixo
  • and e or são usados como palavras-chave de controle de fluxo, e construções if e loop podem opcionalmente omitir chaves, com o formatador garantindo a segurança
  • Sem namespaces, tudo é tratado como expressão, unificando a sintaxe de tipos, valores e padrões, e usando de forma concisa genéricos, literais de registro e funções embutidas (@import, @as etc.)

Visão geral

  • O Zig tem uma aparência semelhante à do Rust, mas adota uma estrutura de linguagem mais simples
  • No design da sintaxe, o foco está em facilidade para grep, consistência sintática e redução de ruído visual desnecessário

Literais inteiros

const an_integer = 92;  
assert(@TypeOf(an_integer) == comptime_int);  
  
const x: i32 = 92;  
const y = @as(i32, 92);  
  • Todos os literais inteiros têm o tipo comptime_int
  • Ao atribuir a uma variável, é preciso declarar o tipo explicitamente ou converter com @as
  • A forma var x = 92; não funciona; é necessário um tipo explícito

Literais de string

const raw =  
    \\Roses are red  
    \\  Violets are blue,  
    \\Sugar is sweet  
    \\  And so are you.  
    \\  
;  
  • Cada linha é um token separado, então não há problema com indentação
  • Não é necessário escapar o próprio \\

Literais de registro

const p: Point = .{  
    .x = 1,  
    .y = 2,  
};  
  • O formato .x = 1 é vantajoso para distinguir leitura e escrita
  • A notação .{} se diferencia de blocos e faz conversão automática para o tipo de resultado

Notação de tipos

u32        // inteiro  
[3]u32     // array de tamanho 3  
?[3]u32    // array anulável  
*const ?[3]u32 // ponteiro constante  
  • Todos os tipos usam notação de prefixo
  • A desreferenciação usa notação de sufixo (ptr.*)

Identificadores

const @"a name with space" = 42;  
  • É possível evitar conflito com palavras-chave ou definir nomes especiais

Declaração de funções

pub fn main() void {}  
fn add(x: i32, y: i32) i32 {  
    return x + y;  
}  
  • A palavra-chave fn fica colada ao nome da função, facilitando a busca
  • Não se usa -> para indicar o tipo de retorno

Declaração de variáveis

const mid = lo + @divFloor(hi - lo, 2);  
var count: u32 = 0;  
  • Usa const e var
  • A notação de tipo segue a ordem nome: tipo

Controle de fluxo: and/or

while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {  
    count -= 1;  
}  
  • and e or são palavras-chave de controle de fluxo
  • Para operações bit a bit, usam-se & e |

Instrução if

.direction = if (prng.boolean()) .ascending else .descending;  
  • Parênteses são obrigatórios; chaves são opcionais
  • O zig fmt garante uma formatação segura

Laços

for (0..10) |i| {  
    print("{d}\n", .{i});  
} else @panic("loop safety counter exceeded");  
  • Tanto for quanto while aceitam cláusula else
  • O iterador e o nome do elemento são posicionados de forma intuitiva

Namespaces e resolução de nomes

const std = @import("std");  
const ArrayList = std.ArrayList;  
  • Sombreamento de variáveis é proibido
  • Não há namespaces nem importações globais

Tudo é expressão

const E = enum { a, b };  
const e: if (true) E else void = .a;  
  • Unifica a sintaxe de tipos, valores e padrões
  • É possível colocar expressões condicionais na posição de tipo

Genéricos

fn ArrayListType(comptime T: type) type {  
    return struct {  
        fn init() void {}  
    };  
}  
  
var xs: ArrayListType(u32) = .init();  
  • Os genéricos são expressos com sintaxe de chamada de função (Type(T))
  • Os argumentos de tipo são sempre explícitos

Funções embutidas

const foo = @import("./foo.zig");  
const num = @as(i32, 92);  
  • Recursos fornecidos pelo compilador são chamados com o prefixo @
  • @import mostra claramente o caminho do arquivo
  • Os argumentos devem ser obrigatoriamente literais de string

Conclusão

  • A sintaxe do Zig é um caso em que um conjunto de pequenas escolhas se junta para criar uma linguagem agradável de ler
  • Quando se reduz o número de recursos, também se reduz a sintaxe necessária e a chance de conflito entre construções sintáticas
  • Ele aproveita boas ideias de linguagens existentes, mas introduz nova sintaxe sem medo quando necessário

1 comentários

 
GN⁺ 2025-08-22
Comentários no Hacker News
  • Este texto trata em profundidade vários trade-offs que surgem no design de sintaxe, e achei realmente impressionantes o minimalismo e a consistência da sintaxe do Zig, além do foco quase implacável em legibilidade. Gosto do fato de isso não ser uma beleza abstrata, mas um tipo de “brutalismo” sem surpresas para uso industrial. Esse tipo de design de sintaxe equilibrado é realmente raro, e acho que o Zig acertou muito bem

    • Fiquei com pena de o artigo não mencionar tratamento de erros. O esquema de try/catch do Zig é excelente, e é meu jeito favorito de lidar com erros entre várias linguagens. Teria sido ainda melhor se isso também tivesse sido apresentado

    • O verdadeiro charme do Zig não está numa “legibilidade superficialmente bonita”, mas numa beleza consistente obtida por abstração. Como na analogia entre S-expressions e M-expressions, uma boa abordagem para o caso geral costuma ser melhor no longo prazo do que um design especial para várias situações excepcionais. Se você adiciona casos de exceção como em C++, no fim só aumenta o peso de ter de decorar todas as regras. No design de linguagens, se você busca simplicidade e consistência, pode acabar caindo num “Turing tarpit” em que a complexidade é toda empurrada para o usuário, então é importante uma abordagem em que casos especiais sejam resolvidos naturalmente a partir de regras gerais. Dá para ver um exemplo disso no quadrinho New Pet do XKCD

    • Se houver algum exemplo que tenha te impressionado, queria saber qual foi

  • Sobre o fato de o Zig usar anotação de tipos no formato “nome: tipo”, como Rust, eu na verdade prefiro o estilo tradicional em que o tipo vem primeiro. Quando vou conferir de novo a declaração de uma variável, o que mais quero saber é o tipo dela, e é incômodo não conseguir achá-lo rápido. Especialmente no Rust, há muita repetição desnecessária como let mut, o que acaba sendo mais trabalhoso, e eu também gosto do estilo de C e C++, com o tipo primeiro. Na prática, acho ideal usar inferência de tipos só no mínimo necessário

    • A palavra-chave let também é necessária porque deixa claro que se trata de uma declaração. Sem isso, você pode acabar enfrentando os problemas de parsing ambíguo do C++

    • Eu também sempre tento verificar primeiro o tipo da variável, então prefiro o estilo em que o tipo vem antes. Do ponto de vista do parser, processar o nome primeiro é conveniente, e entendo que o TypeScript adotou essa estrutura por compatibilidade com JavaScript. No fim, acho que o mais importante é uma biblioteca padrão fácil de usar. Como nos exemplos de abuso excessivo do sistema de tipos, em vez de expressar todo estado possível como tipo, o mais importante é comunicar a intenção com clareza

    • Eu volto no código para verificar o tipo da variável, mas, curiosamente, quando o tipo vem primeiro fica mais difícil achar a declaração da variável que estou procurando. Como o nome do tipo fica logo no começo e seu tamanho varia, tenho de mover os olhos repetidamente para os lados, o que parece ineficiente

    • Na maioria dos casos, o editor mostra a informação de tipo imediatamente ao passar o mouse, então a posição do tipo no código talvez não seja tão importante. O Rust é verboso em grande parte por razões de implementação para evitar ambiguidades de parsing. Quando o tipo vem primeiro, como em C e C++, fica mais difícil encontrar com grep variáveis declaradas com um determinado nome, e o estilo de colocar o tipo de retorno antes surgiu por causa de templates, mas dependendo do caso também pode facilitar a leitura e a busca no código

    • Pessoalmente, prefiro mais o estilo de anotação de tipos do Pascal. Mesmo com inferência de tipos, não é preciso um recurso indireto separado como auto, e do ponto de vista do parsing também é menos ambíguo. Em MyClass x, não dá para saber de imediato se MyClass é um tipo ou o nome de uma variável, então isso reduz essa ambiguidade

  • Sobre a sintaxe de raw/multiline strings do Zig, o jeito de ter de escrever \\ várias vezes parece confuso demais e extremo

    • Se você já formatou strings multilinha em Python, C++, Rust etc., vai entender esse incômodo. Sempre existe a questão da indentação acabar fazendo parte do conteúdo da string, e quando há um modo de remover indentação, como no YAML, isso às vezes só aumenta a confusão. O jeito do Zig é extremamente claro no que diz respeito à indentação

    • No começo essa sintaxe me pareceu muito incômoda, mas, à medida que você usa Zig, vai se acostumando e até passa a enxergar as vantagens. O Zig tem um lado curioso: no primeiro contato ele pode causar rejeição, mas ao usar de verdade você percebe os benefícios

    • Na verdade não é uma sintaxe maluca, e sim um problema maluco a ser resolvido com segurança: colocar uma string multilinha dentro de outra string multilinha. No Zig, gosto do fato de não precisar de escapes separados nem me preocupar com indentação

    • trimIndent do Kotlin, os text blocks do Go ou Java, e especialmente a raw string com crases do Go, me parecem mais suaves. No Zig, por causa do \\, eu acabo contornando isso usando @embedFile

    • Visualmente eu não gosto muito do \\, mas acho que é uma forma limpa de resolver o problema de literais multilinha e indentação. Não conheço outra linguagem que resolva isso sem recorrer a função

  • A sintaxe do Zig parece um pouco dispersa. Construções que começam com @, como @TypeOf, ou a sintaxe de inicialização . {.x} me soam estranhas. Talvez seja porque eu não tenha tanta prática com Zig, mas no geral fico com a impressão de que o código é difícil de ler

    • Prefiro a sintaxe do Odin, que é bem mais minimalista e refinada. O Zig me passa uma sensação um pouco mais dispersa

    • O . funciona no Zig como um placeholder para tipo inferido. Por exemplo, você pode inicializar um objeto assim

      const p = Point{ .x = 123, .y = 234 };
      

      Ou, se quiser deixar explícita a inferência de tipo,

      const p: Point = .{ .x = 123, .y = 234 };
      

      Também dá para omitir o tipo em argumentos de função, o que deixa tudo mais conciso. Em Rust, você precisa escrever o tipo explicitamente nessas situações

      takePoint(Point{ x: 123, y: 234 });
      

      Em inicialização de structs aninhadas, a inferência do Zig também é muito mais útil. No Rust, ter de escrever explicitamente o tipo em todo lugar pode deixar o código poluído muito rápido. Mesmo assim, eu acharia mais conveniente remover a notação com ponto à frente, mas parece que ela foi mantida para simplificar a implementação do parser. As notações x: 123 e .x = 123 foram emprestadas de JS e C99, respectivamente. Pessoalmente uso as duas com frequência, então não me parecem estranhas

  • Prefiro muito mais a forma de raw string literal do C# 11. Ela ajusta automaticamente a indentação das demais linhas com base na indentação da primeira linha. Também dá para usar chaves literalmente. Se houver vários $, as chaves passam a ser tratadas completamente como valor

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (Como autor do recurso de raw string literal do C#) na verdade a referência é a indentação da linha final com """, e a primeira linha também pode ser indentada. Fico feliz que tenha gostado desse recurso, e me orgulho dele como uma boa funcionalidade
  • A sintaxe do Zig é boa, mas, considerando que dá para escrever de forma suficientemente limpa sem ponto e vírgula ou : como em Go, não acho que chegue a ser “lovely”. Se for para comparar, com certeza é uma grande melhora em relação ao Rust, mas o Go também é muito bom

    • Pelo contrário, uma sintaxe minimalista demais como a do Go às vezes fica mais difícil de interpretar na leitura. Como passamos mais tempo lendo código do que escrevendo, uma concisão excessiva pode acabar causando mais erros e dificultando o debugging. CoffeeScript e J são exemplos clássicos de sintaxes excessivamente abreviadas

    • Não acho que remover elementos sintáticos torne a sintaxe automaticamente melhor. Se fosse assim, todo mundo escreveria como Lisp, e também escreveríamos textos como em scriptio continua, o antigo estilo sem espaços. Veja scriptio continua na Wikipédia

  • No geral estou satisfeito com o Zig, mas os seguintes pontos deixam a desejar

    • É difícil especificar o valor de retorno de um bloco. Seria bom se, como no Rust, a última expressão fosse reconhecida automaticamente como valor de retorno, mas no Zig é preciso usar label e outras coisas, o que é trabalhoso
    • Não é possível fazer chaining com tipos opcionais, como a?.b?.c. Se houvesse suporte a tipos monádicos, daria para fazer um encadeamento mais geral, mas ainda falta isso
    • Não há suporte a funções lambda. Já que a linguagem já usa blocos de função em lugares como loops ou blocos catch, ter suporte a lambda deixaria isso ainda mais flexível
  • Sobre usar void como nome de tipo, na verdade, na teoria dos tipos, void não exerce o papel de unit, mas sim de tipo “uninhabited”, sem valores. Tradicionalmente, () ou unit é que são o tipo com um único membro. void seria o tipo de retorno de uma função como abort

    • Em C e C++, void é usado de forma razoavelmente adequada e é familiar para muitos programadores de sistemas. Acho que essa disputa terminológica da teoria dos tipos é irrelevante no uso prático. Como muita gente que chega ao Zig vem do C e do C++, void está perfeitamente bom

    • abort tem um tipo para estado “inatingível”, como o ! do Rust. void está mais próximo de unit ou (), um tipo em que não existem valores. Como curiosidade, no TypeScript dá para usar void em restrições genéricas e tornar aquele parâmetro opcional

    • O tipo void tem uma tradição muito longa e remonta até ao ALGOL 68. Lá, o tipo VOID é definido como um tipo com um único membro (EMPTY)

  • Fiquei surpreso com a ideia de que “o Zig não tem lambdas”. Em C++, eu uso lambda praticamente o tempo todo, então fico curioso sobre como se define, por exemplo, um comparator para ordenar arrays

    • Como normalmente é preciso declarar uma função separadamente, acho essa parte do Zig incômoda

    • Dá para usar uma struct anônima e referenciar inline uma função contida nela. Na prática, o recurso de captura, que costuma ser o principal nas lambdas, não existe no Zig, mas dá para substituir isso passando um parâmetro de contexto, geralmente uma struct

    • Basicamente, do mesmo jeito que em C: você declara uma função separada e passa o ponteiro dela para a função de ordenação

  • As pessoas dizem que “sintaxe não importa”, mas na prática isso muitas vezes vira “sintaxe não importa, então vamos usar do jeito que eu prefiro”. Eu também estou acostumado com sintaxes derivadas da família C, como Rust/Zig/Go, e estilos como o de Haskell/OCaml, que distinguem chamada de função por espaços, ainda me parecem estranhos e um obstáculo à popularização. Como no sucesso do Rust, talvez valha a pena outras linguagens aprenderem com a ideia de misturar bem o “espinafre” da programação funcional dentro do “brownie” de uma linguagem de sistemas

    • Não concordo com a ideia de que sintaxe não importa. No fim, a sintaxe é a principal interface pela qual o usuário interage com a linguagem. Sempre que leio uma linguagem, os elementos sintáticos acabam se destacando mais do que eu gostaria, de forma inconsciente

    • Se você quer uma linguagem funcional com sintaxe da família C, recomendo Gleam: gleam.run O código também é muito bonito

      fn spawn_greeter(i: Int) {
       process.spawn(fn() {
        let n = int.to_string(i)
        io.println("Hello from "  n)
       })
      }
      

      Reason também vale a recomendação. É baseada em OCaml, mas tem sintaxe da família C: reasonml.github.io