- 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
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/catchdo Zig é excelente, e é meu jeito favorito de lidar com erros entre várias linguagens. Teria sido ainda melhor se isso também tivesse sido apresentadoO 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árioA palavra-chave
lettambé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
grepvariá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ódigoPessoalmente, 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. EmMyClass x, não dá para saber de imediato seMyClassé um tipo ou o nome de uma variável, então isso reduz essa ambiguidadeSobre a sintaxe de raw/multiline strings do Zig, o jeito de ter de escrever
\\várias vezes parece confuso demais e extremoSe 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
trimIndentdo 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@embedFileVisualmente 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çãoA 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 lerPrefiro 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 assimOu, se quiser deixar explícita a inferência de tipo,
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
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: 123e.x = 123foram emprestadas de JS e C99, respectivamente. Pessoalmente uso as duas com frequência, então não me parecem estranhasPrefiro 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""", e a primeira linha também pode ser indentada. Fico feliz que tenha gostado desse recurso, e me orgulho dele como uma boa funcionalidadeA 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 bomPelo 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
a?.b?.c. Se houvesse suporte a tipos monádicos, daria para fazer um encadeamento mais geral, mas ainda falta issocatch, ter suporte a lambda deixaria isso ainda mais flexívelSobre usar
voidcomo nome de tipo, na verdade, na teoria dos tipos,voidnão exerce o papel deunit, mas sim de tipo “uninhabited”, sem valores. Tradicionalmente,()ouunité que são o tipo com um único membro.voidseria o tipo de retorno de uma função comoabortEm 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++,voidestá perfeitamente bomaborttem um tipo para estado “inatingível”, como o!do Rust.voidestá mais próximo deunitou(), um tipo em que não existem valores. Como curiosidade, no TypeScript dá para usarvoidem restrições genéricas e tornar aquele parâmetro opcionalO tipo
voidtem uma tradição muito longa e remonta até ao ALGOL 68. Lá, o tipoVOIDé 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
Reason também vale a recomendação. É baseada em OCaml, mas tem sintaxe da família C: reasonml.github.io