7 puntos por GN⁺ 2025-08-22 | 1 comentarios | Compartir por WhatsApp
  • Zig se basa en una sintaxis con llaves similar a la de Rust, pero la mejora con una semántica del lenguaje más simple y decisiones sintácticas más refinadas
  • Los literales enteros comienzan todos con el tipo comptime_int y se convierten explícitamente al asignarse, mientras que los literales de cadena usan una notación concisa de cadenas raw basada en \\
  • Los literales de registro en la forma .x = 1 hacen que la escritura de campos sea fácil de buscar, y todos los tipos se expresan de forma consistente con notación de prefijo
  • Usa and y or como palabras clave de control de flujo, y las construcciones if y loop pueden omitir llaves opcionalmente, mientras el formateador garantiza la seguridad
  • Sin namespaces, todo se trata como una expresión, unificando la sintaxis de tipos, valores y patrones, y permitiendo usar de forma concisa genéricos, literales de registro y funciones integradas (@import, @as, etc.)

Resumen general

  • Zig tiene una apariencia similar a Rust, pero adopta una estructura de lenguaje más simple
  • El diseño de su sintaxis se enfoca en la facilidad para buscar con grep, la consistencia sintáctica y la reducción del ruido visual innecesario

Literales enteros

const an_integer = 92;  
assert(@TypeOf(an_integer) == comptime_int);  
  
const x: i32 = 92;  
const y = @as(i32, 92);  
  • Todos los literales enteros son de tipo comptime_int
  • Al asignarlos a una variable, hay que indicar el tipo explícitamente o convertirlos con @as
  • La forma var x = 92; no funciona; se requiere un tipo explícito

Literales de cadena

const raw =  
    \\Roses are red  
    \\  Violets are blue,  
    \\Sugar is sweet  
    \\  And so are you.  
    \\  
;  
  • Cada línea es un token independiente, así que no hay problemas de indentación
  • No hace falta escapar \\ en sí mismo

Literales de registro

const p: Point = .{  
    .x = 1,  
    .y = 2,  
};  
  • El formato .x = 1 ayuda a distinguir mejor entre lectura y escritura
  • La notación .{} se diferencia de un bloque y se convierte automáticamente al tipo de resultado

Notación de tipos

u32        // entero  
[3]u32     // arreglo de longitud 3  
?[3]u32    // arreglo anulable  
*const ?[3]u32 // puntero constante  
  • Todos los tipos usan notación de prefijo
  • La desreferenciación usa notación de sufijo (ptr.*)

Identificadores

const @"a name with space" = 42;  
  • Permite evitar conflictos con palabras clave o asignar nombres especiales

Declaración de funciones

pub fn main() void {}  
fn add(x: i32, y: i32) i32 {  
    return x + y;  
}  
  • La palabra clave fn y el nombre de la función van juntos, lo que facilita la búsqueda
  • No usa -> para indicar el tipo de retorno

Declaración de variables

const mid = lo + @divFloor(hi - lo, 2);  
var count: u32 = 0;  
  • Usa const y var
  • La anotación de tipo sigue el orden nombre: tipo

Control de flujo: and/or

while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {  
    count -= 1;  
}  
  • and y or son palabras clave de control de flujo
  • Para operaciones bit a bit se usan &, |

Sentencia if

.direction = if (prng.boolean()) .ascending else .descending;  
  • Los paréntesis son obligatorios; las llaves son opcionales
  • zig fmt garantiza un formato seguro

Bucles

for (0..10) |i| {  
    print("{d}\n", .{i});  
} else @panic("loop safety counter exceeded");  
  • Tanto for como while admiten cláusula else
  • La disposición del iterador y del nombre del elemento resulta intuitiva

Namespaces y resolución de nombres

const std = @import("std");  
const ArrayList = std.ArrayList;  
  • No se permite el shadowing de variables
  • No hay namespaces ni imports globales

Todo es una expresión

const E = enum { a, b };  
const e: if (true) E else void = .a;  
  • Unifica la sintaxis de tipos, valores y patrones
  • Se pueden usar expresiones condicionales en la posición de un tipo

Genéricos

fn ArrayListType(comptime T: type) type {  
    return struct {  
        fn init() void {}  
    };  
}  
  
var xs: ArrayListType(u32) = .init();  
  • Los genéricos se expresan con sintaxis de llamada a función (Type(T))
  • Los argumentos de tipo siempre son explícitos

Funciones integradas

const foo = @import("./foo.zig");  
const num = @as(i32, 92);  
  • El prefijo @ invoca funciones provistas por el compilador
  • @import muestra claramente la ruta del archivo
  • El argumento debe ser obligatoriamente un literal de cadena

Conclusión

  • La sintaxis de Zig es un caso donde un conjunto de pequeñas decisiones se junta para crear un lenguaje agradable de leer
  • Al reducir la cantidad de funciones del lenguaje, también se reduce la sintaxis necesaria y disminuye la posibilidad de choques entre construcciones
  • Toma prestadas las buenas ideas de lenguajes existentes, pero cuando hace falta introduce con decisión una sintaxis nueva

1 comentarios

 
GN⁺ 2025-08-22
Comentarios de Hacker News
  • Este artículo aborda a fondo varios trade-offs que aparecen en el diseño de sintaxis, y me impresionó mucho el minimalismo y la consistencia de la sintaxis de Zig, así como su enfoque implacable en la legibilidad. Me gusta que no sea una belleza abstracta, sino una especie de “brutalismo” donde no hay sorpresas para el uso industrial. Un diseño de sintaxis tan equilibrado es realmente raro, y creo que Zig lo logró muy bien

    • Me dio pena que el artículo no mencionara el manejo de errores. La forma en que Zig usa try/catch es excelente; de muchos lenguajes, es mi manera favorita de manejar errores. Habría estado aún mejor si también hubieran cubierto esa parte

    • El verdadero encanto de Zig no está en una “legibilidad superficialmente bonita”, sino en la belleza consistente que se obtiene mediante la abstracción. Como en la analogía entre S-expressions y M-expressions, muchas veces un buen enfoque para el caso general termina siendo mejor a largo plazo que un diseño especial para varias situaciones excepcionales. Si empiezas a agregar toda clase de casos especiales como en C++, al final solo aumenta la carga de tener que memorizar todas las reglas. En diseño de lenguajes, si persigues simplicidad y consistencia, puedes terminar cayendo en un “Turing tarpit” donde la complejidad la absorbe el usuario; por eso es importante un enfoque donde los casos especiales se resuelvan de forma natural a partir de reglas generales. También puede verse un ejemplo de esto en el cómic de XKCD New Pet

    • Si alguien tiene un ejemplo que le haya parecido especialmente impresionante, me gustaría que lo compartiera

  • Sobre el hecho de que Zig use una anotación de tipos estilo Rust con formato “nombre:tipo”, en realidad me sigue gustando más la forma tradicional donde el tipo va primero. Cuando vuelvo a revisar una declaración de variable, lo que más quiero saber es el tipo de esa variable, y si no puedo encontrarlo rápido me resulta incómodo. En Rust, especialmente, hay muchos elementos repetitivos e innecesarios como let mut, lo que termina siendo más engorroso; también me gusta que el tipo vaya primero como en C y C++. En la práctica, creo que lo ideal es usar inferencia de tipos solo al mínimo, donde realmente se necesita

    • La palabra clave let también tiene su razón de ser, porque deja claro que se trata de una declaración. De otro modo podrías encontrarte con problemas de parseo ambiguo como en C++

    • Yo también tiendo a revisar primero el tipo de una variable, así que prefiero el estilo donde el tipo va antes. Desde la perspectiva del parser, procesar primero el nombre es más conveniente, y entiendo que TypeScript adoptó esa estructura por compatibilidad con JavaScript. Al final, creo que lo importante es tener una biblioteca estándar fácil de usar. Como en esos ejemplos donde se abusa demasiado del sistema de tipos, a veces es más importante transmitir claramente la intención que intentar expresar todos los estados como tipos

    • Uno vuelve hacia arriba en el código para comprobar el tipo de una variable, pero si el tipo va primero, paradójicamente se vuelve más difícil encontrar la declaración de variable que estás buscando. Como el nombre del tipo aparece al inicio y su longitud es variable, terminas moviendo la vista de lado a lado repetidamente, y eso me parece ineficiente

    • En la mayoría de los casos, el editor te muestra enseguida la información de tipos al pasar el mouse, así que la ubicación del tipo en el código quizá no sea tan importante. Rust es verboso en gran parte por cuestiones de implementación para evitar ambigüedades de parseo. Si el tipo va primero como en C y C++, no es tan fácil buscar con grep variables declaradas con un nombre específico, y el estilo de poner el return type delante surgió por los templates, aunque a veces sí hace el código más fácil de leer y de encontrar

    • Personalmente prefiero más el estilo Pascal para anotar tipos. Incluso cuando usas inferencia de tipos, no necesitas una función de escape aparte como auto, y desde el punto de vista del parseo también es menos ambiguo. En algo como MyClass x, no queda claro de inmediato si MyClass es un tipo o el nombre de una variable, así que ayuda a reducir esa ambigüedad

  • La sintaxis de raw/multiline string de Zig, con esa necesidad de escribir \\ varias veces, me parece demasiado confusa y extrema

    • Si alguna vez has dado formato a strings multilínea en Python, C++, Rust u otros lenguajes, entenderás esa incomodidad. Siempre está el problema de que la indentación se incluye en el contenido del string, y cuando hay modos de eliminar indentación como en YAML, a veces más bien aumentan la confusión. En ese aspecto, el enfoque de Zig es muy claro respecto a la indentación

    • Al principio esta sintaxis me parecía muy incómoda, pero conforme usas Zig te vas acostumbrando y hasta empiezas a verle las ventajas. Zig tiene eso de que al principio puede generar rechazo, pero al usarlo de verdad terminas entendiendo sus virtudes

    • En realidad no es que sea una sintaxis loca, sino que intenta resolver un problema loco y complejo: meter de forma segura un string multilínea dentro de otro string multilínea. En Zig me gusta que no necesites escapes adicionales ni preocuparte por la indentación

    • trimIndent de Kotlin, los text blocks de Go o Java, y sobre todo los raw strings con backticks de Go me resultan más fluidos. En Zig, por culpa de \\, termino evitando eso y usando @embedFile como workaround

    • Visualmente no me encantan los \\, pero sí creo que es una forma limpia de resolver el problema de los literales multilínea y la indentación. No conozco realmente otro lenguaje que resuelva esto sin recurrir a funciones

  • La sintaxis de Zig me parece algo cargada. Construcciones que empiezan con @ como @TypeOf o la sintaxis de inicialización como .{.x} se me hacen raras. Puede ser porque no tengo mucha práctica usando Zig, pero en general me da la impresión de que el código es difícil de leer

    • Prefiero la sintaxis de Odin; me parece mucho más minimalista y pulida. Zig sí me da una sensación algo cargada

    • . actúa en Zig como un placeholder para un tipo inferido. Por ejemplo, puedes inicializar un objeto así

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

      o, si quieres expresar explícitamente la inferencia de tipo,

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

      También puedes omitir el tipo en los argumentos de funciones, lo que lo hace más conciso. En Rust tendrías que escribir el tipo explícitamente en ese tipo de situación

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

      Incluso en la inicialización de structs anidados, la inferencia de Zig es mucho más útil. En Rust, tener que escribir el tipo explícitamente en todas partes puede volver el código bastante cargado rápidamente. Aun así, creo que sería más cómodo omitir el punto inicial, pero parece que lo mantienen para simplificar la implementación del parser. La notación x: 123 o .x = 123 viene tomada de JS y C99, respectivamente. Personalmente uso ambas con frecuencia, así que no me parecen raras

  • Prefiero mucho más la forma de raw string literal de C# 11. Toma la indentación de la última línea como base y alinea automáticamente la de las demás líneas. Además, puedes usar llaves como caracteres literales. Si aparecen varios $, las llaves se tratan completamente como texto

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (Como autor de la función de raw string literal de C#) en realidad la referencia es la indentación de la línea final con """, y la primera línea también puede llevar indentación. Me alegra que te guste esta función, y estoy orgulloso de decir que es una buena función
  • La sintaxis de Zig me gusta, pero no diría que llega a ser “lovely” cuando Go demuestra que también se puede escribir de forma bastante limpia incluso sin punto y coma ni :. Si hubiera que comparar, sí, está mucho mejor que Rust, pero Go también es suficientemente bueno

    • Una sintaxis excesivamente minimalista como la de Go a veces puede ser más difícil de interpretar al leerla. Pasamos más tiempo leyendo código que escribiéndolo, así que una concisión excesiva puede provocar errores y dificultar el debugging. CoffeeScript o J son ejemplos representativos de sintaxis demasiado comprimidas

    • No creo que quitar elementos sintácticos haga automáticamente mejor a una sintaxis. Si así fuera, todos escribiríamos como Lisp, e incluso escribiríamos textos como en scriptio continua, el antiguo estilo sin espacios. Véase scriptio continua en Wikipedia

  • En general estoy satisfecho con Zig, pero me dejan debiendo estos puntos

    • Es difícil especificar el valor de retorno de un bloque. Estaría bien que, como en Rust, la última expresión se reconociera automáticamente como valor de retorno, pero en Zig hay que usar labels y eso es engorroso
    • No se puede hacer chaining de tipos opcionales (por ejemplo, a?.b?.c). Si hubiera soporte para tipos monádicos, se podría tener un chaining más general, pero todavía falta
    • No hay soporte para funciones lambda. Ya se usan bloques de función en lugares como loops o bloques catch, así que con soporte para lambdas podría ser más flexible
  • Sobre usar void como nombre de tipo, en teoría de tipos void no cumple realmente el papel de unit, sino que representa un tipo no habitado, sin valores. Tradicionalmente () o unit son tipos con un solo miembro. void sería más bien el tipo de retorno de funciones como abort

    • En C y C++, void se ha usado bastante bien desde hace mucho, así que a muchos programadores de sistemas les resulta familiar. Creo que discutir la terminología de teoría de tipos no tiene mucha importancia en el uso práctico. Mucha gente que llega a Zig viene de C o C++, así que void está perfectamente bien

    • abort corresponde más a un tipo para estados “inalcanzables”, como el tipo ! de Rust. void en realidad está más cerca de unit o (), como un tipo donde no hay un valor significativo. Como truco curioso, en TypeScript, si usas void en una restricción genérica, puedes hacer que ese parámetro sea opcional

    • El tipo void tiene una tradición larguísima, remontándose hasta ALGOL 68. Ahí el tipo VOID se define como un tipo con un solo miembro (EMPTY)

  • Me sorprende eso de que “Zig no tiene lambdas”. En C++ uso lambdas casi en todos lados, así que me pregunto cómo se define, por ejemplo, un comparator para ordenar un arreglo

    • Normalmente se hace declarando una función aparte, y en ese sentido Zig me parece incómodo

    • Puedes referenciar inline un struct anónimo y funciones contenidas dentro de él. Es cierto que Zig no tiene la funcionalidad de captura que suele usarse en lambdas, pero eso puede sustituirse pasando un parámetro de contexto, normalmente un struct

    • Básicamente, igual que en C: declaras una función separada y luego pasas su puntero a la función de ordenamiento

  • Se dice que “la sintaxis no importa”, pero en la práctica suele significar “la sintaxis no importa, así que usemos la que yo prefiero”. A mí también me resulta familiar la sintaxis derivada de C, como la de Rust/Zig/Go, y estilos como Haskell u OCaml, donde las llamadas a funciones se distinguen por espacios, todavía se me hacen ajenos y creo que eso dificulta su popularización. Como en el éxito de Rust, tal vez otros lenguajes podrían aprender de cómo mezcló bien la “espinaca” de la programación funcional dentro del “brownie” de un lenguaje de sistemas

    • No estoy de acuerdo con eso de que la sintaxis no importa. Al final, la sintaxis es la interfaz principal con la que el usuario interactúa con el lenguaje. Cada vez que leo un lenguaje, los elementos sintácticos terminan resaltando más en mi mente de forma inconsciente

    • Si quieres un lenguaje funcional con sintaxis estilo C, recomiendo Gleam: gleam.run El código también se ve muy bonito

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

      También vale la pena recomendar Reason. Está basado en OCaml, pero tiene sintaxis estilo C: reasonml.github.io