- El compilador de Zig, que incluye de forma nativa compilación de código C y compilación cruzada, es el lenguaje más sorprendente que el autor ha conocido en 45 años de experiencia
- Con funciones únicas como ejecución en tiempo de compilación, variables de tamaño de bits arbitrario y entorno de bloques de prueba, va más allá de ser un simple reemplazo de C/C++ y ofrece una forma completamente nueva de programar
- Con una sintaxis concisa y clara, como declaración de variables mediante inferencia de tipos, structs anónimos y break con etiquetas, se puede aprender rápidamente
- Con pruebas independientes de módulos mediante bloques de prueba y la función integrada
@breakpoint, permite depurar código optimizado
- Con soporte para programación de bajo nivel usando campos de bits y operaciones bit a bit, logra eficiencia y robustez al mismo tiempo, integrando en un lenguaje compilado ventajas propias de los lenguajes interpretados
Prólogo
- En 45 años de experiencia, no hubo ningún lenguaje tan sorprendente como Zig
- Zig no es simplemente un lenguaje nuevo, sino una herramienta que cambia de raíz la forma de programar
- Verlo solo como un reemplazo de C o C++ es una gran subestimación
- El objetivo de este texto es presentar las funciones simples pero atractivas de Zig y ayudar a que los programadores puedan empezar rápido
- Existen muchas más funciones que influyen en la adopción de Zig en la industria
El compilador de Zig
- Incluye por defecto compilación de código C y compilación cruzada sin configuración adicional, lo que tiene un gran impacto en la industria
- La instalación consiste en descargar el compilador correspondiente al procesador/SO desde la página de descargas de Zinglang, descomprimirlo y copiarlo al directorio deseado
- En Windows 10, se copia el archivo zip x86_64 en "Program Files" y se cambia el nombre del directorio raíz a "zig-windows-x86_64" para no tener que modificar la variable de entorno Path al actualizar la versión
- Tras agregar la ruta del directorio raíz a la variable de entorno Path, el compilador puede usarse en modo CLI
- Para compilar el programa "Hello World!", se recomienda consultar la sección "Getting Started" del sitio oficial
Conceptos y comandos principales
Declaración de variables
- La declaración de variables se compone de una primera parte con accesibilidad (
pub u omitida), var/const y nombre de la variable, una segunda parte con la declaración del tipo y una tercera con la inicialización
- Solo la primera y la tercera parte son obligatorias, y el tipo puede inferirse a partir del valor de inicialización
- Ejemplo:
var sum : usize = 0;
- Las variables declaradas sin
pub solo son accesibles dentro del módulo (similar a las variables static en C)
- No se recomienda declarar variables
pub, y se aconseja minimizar las funciones pub para reducir el acoplamiento y aumentar la cohesión
Structs, structs anónimos y bloques de prueba
- Los literales de struct anónimo rodeados por
.{ y } se usan para inicializar elementos de otros structs o crear nuevos structs con sus elementos inicializados
.{ } es un literal de struct anónimo vacío
- La forma
struct { } es una declaración de struct
- Los bloques de prueba permiten compilar y ejecutar pruebas sin necesidad de un ejecutable
Campos de bits
- Los campos de bits se declaran como campos con tipos de tamaño específico dentro de un
packed struct
- Los punteros pueden apuntar a un campo de bits específico
Bucle for
- La sintaxis de Zig es más clara que la de C, pero usa un intervalo abierto
[0..9) en lugar de [0..8]
- La declaración de tipo, inicialización, prueba e incremento de la variable de bucle
i se manejan automáticamente
Arreglos
[_] define un arreglo cuyo tamaño no se conoce explícitamente, seguido por el tipo de elemento y la inicialización
- Ejemplo:
var grid = [_]u8{0} ** 81; inicializa 81 elementos u8 con 0
- El tamaño del arreglo se infiere a partir del argumento de repetición de la inicialización
- En el entorno de prueba, se puede recorrer el arreglo y acumular sus elementos
- Las variables declaradas entre
| en un bucle for se asumen automáticamente del mismo tipo que los elementos del arreglo
usize es el entero sin signo natural de la plataforma (u64 en 64 bits, u32 en 32 bits)
Punteros de múltiples elementos
- Para que un puntero a arreglo pueda usar aritmética de punteros, debe declararse explícitamente como puntero de múltiples elementos, por ejemplo
[*]const i32
- Aunque el arreglo sea
const, el puntero puede declararse como var
Desreferenciación de punteros
- Un puntero al que se le asigna la dirección de una posición individual de un arreglo no puede actualizarse con aritmética de punteros
- La desreferenciación de punteros se hace con
ptr.*
Break con etiqueta
- Es posible realizar diversas tareas en tiempo de compilación, como inicializar arreglos
- El break con etiqueta pone
: después del nombre del bloque y devuelve un valor desde el bloque con break
0.. es un rango infinito que comienza en 0
- En un bucle
for, las variables se inicializan e incrementan automáticamente, y el bucle termina después de procesar la última posición del arreglo
- Un arreglo puede no inicializarse explícitamente con
undefined
Funciones en Zig
- Las funciones se declaran con
fn y por defecto son static (solo se usan dentro del archivo)
- Si se declaran como
pub fn, pueden importarse desde otros archivos
- Las funciones pueden ser "inlined"
- En los punteros a función,
const va al frente y luego sigue el prototipo de la función
Programación orientada a objetos en Zig
- Los structs pueden tener funciones
- En el ejemplo de la pila, pueden almacenarse hasta 81 elementos del tipo
StkNode
- Los operadores
++ y -- no existen en Zig; se usan += y -=
- El puntero de pila es un entero usado como índice del arreglo
stk
- El puntero
self no se pasa explícitamente como parámetro, sino que se asume indirectamente como el puntero a la instancia de pila sobre la que se llama la función
- En una llamada como
stack.pop(), self es un puntero a stack (similar a this en Java/C++)
- La función
init() es el constructor de la pila
- Las funciones
pop y push son "inlined"
Compilar y ejecutar programas Zig
Compilar un ejecutable
- Para generar un ejecutable se necesita una función
main que represente el punto de entrada del programa
- En programas simples, la función
main puede incluirse en el mismo archivo
- Para depurar módulos de forma independiente, se puede insertar una función
main al final del archivo y comentarla después de terminar la depuración
- Comando de compilación:
zig build-exe -O ReleaseFast program.zig
Ejecutar bloques de prueba de un módulo
- Es una de las mejores funciones de Zig, y se usa para pruebas y prototipado
- Los bloques de prueba comienzan con
test "message" { y terminan con }
- "message" es la cadena que se muestra al ejecutar la prueba
- Los bloques de prueba se ejecutan independientemente del ejecutable, y el ejecutable final no los ejecuta
- Comando de prueba:
zig test module.zig
- El bloque de prueba de example.zig prueba las funciones
set y print; set recibe una cadena decimal como parámetro y print imprime el encabezado "Input Grid" seguido del grid
Salida en Zig
- La instrucción
std.debug.print llama a la función print de debug.zig en la biblioteca estándar de Zig, std
- El primer parámetro es una cadena de formato, y el segundo es un struct anónimo que contiene la lista de variables a mostrar
- Si no hay formato, el struct está vacío
- Por defecto se muestra en stderr
- A diferencia de
printf en C, Zig puede procesar en tiempo de compilación la cadena literal y la lista de variables
Depuración de ejecutables
- Usar un depurador no es simple salvo en IDEs con depurador integrado (Eclipse, IntelliJ IDEA) o kits de desarrollo integrados (
w64devkit)
- Integrar símbolos hace crecer el código y exige compilar en modo Debug, lo que produce código ejecutable notablemente menos eficiente
- Zig ofrece una solución práctica para evitar estos problemas
Función integrada @breakpoint
- Insertar
@breakpoint(); en el código fuente hace que, al ejecutarse bajo un depurador, el programa se detenga en ese punto
- Es una función útil para depurar código Zig optimizado sin símbolos
- Si se usa
std.debug.print justo antes de @breakpoint(); para imprimir las variables que se quieren rastrear, se pueden verificar sus valores en ese instante
- En el ejemplo debug_example.zig, se insertan código para imprimir
grid y variables dentro de la función set, junto con @breakpoint();
- Comando de compilación:
zig build-exe debug_example.zig
- Luego se invoca
debug_example.exe con un depurador como gdb y se ejecuta el programa con el comando r
- Con el comando
c se continúa la ejecución mientras se sigue el contenido de grid y las variables
- Si se presiona Enter repetidamente para continuar, se puede comprobar que los valores de
grid coinciden con el bloque de prueba de example.zig
Programación de bajo nivel en Zig
Representación de la matriz
- Los dígitos decimales se almacenan en la matriz como enteros estándar
u8
- El grid de entrada tiene formato de cadena, pero los caracteres ASCII se convierten internamente en enteros
u8
- El almacenamiento de números se organiza linealmente, fila por fila, en el arreglo
grid de 81 posiciones: var grid = [_]u8{0} ** 81;
- Para verificar la exactitud del
grid, hace falta acceder a los elementos por fila y por columna
- Se crea un arreglo de 9 punteros, cada uno apuntando al inicio de una fila
- Se usa break con etiqueta para devolver un valor desde un bloque de código:
break :fill9x9 m; inicializa matrix con m
- Notación de acceso a elementos:
element = matrix[i][j]
Representar dígitos decimales como bits
- El concepto clave consiste en reemplazar el dígito decimal entero
i por el entero code
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
- La única posición de bit en la que
code tiene valor 1 es i-1 (cuando i está entre 1 y 9); en caso contrario, todos los bits son 0
- Se muestra una tabla con los valores de
code para cada dígito (1→1, 2→2, 3→4, ..., 9→256)
Cálculo de code en Zig
- El valor
code se calcula con el operador de desplazamiento a la izquierda solo cuando c no es 0: code = @as(u9,1) << (c-1);
- En Zig, las constantes deben tener un tamaño adecuado para que la operación se compile y el resultado se asigne a una variable
code se declara como tipo u9 (el valor máximo 256 requiere al menos 9 bits)
- Zig permite variables con tamaño de bits arbitrario
- La función integrada
@as convierte la constante 1 al tipo u9
Representación del grid con campos de bits
Grid de campos de bits por fila
- El arreglo
lines refleja todo el grid representando cada fila como un entero de 9 bits: var lines = [_]u9{0} ** 9;
- Al acceder al arreglo con la fila
i, se verifica con una operación bit a bit AND (&) si un número específico ya existe en esa fila: lines[i] & code
- Si el resultado es 0, el número aún no está en la fila
i; de lo contrario, está duplicado
Grid de campos de bits por columna
- El arreglo
columns refleja todo el grid representando cada columna como un entero de 9 bits: var columns = [_]u9{0} ** 9;
- Al acceder al arreglo con la columna
j, se verifica con una operación AND bit a bit si un número específico ya existe en esa columna: columns[j] & code
- Si el resultado es 0, el número aún no está en la columna
j; de lo contrario, está duplicado
Reglas del Sudoku
- Al insertar un nuevo número en un grid de Sudoku vacío, no debe existir ya en toda la fila, columna y celda que contienen al nuevo elemento
- Una celda es cada uno de los 9 grids de 3x3 separados por líneas gruesas
- Cada elemento específico del grid 9x9 pertenece a una fila, una columna y una celda únicas
- En el grid de ejemplo, la primera celda contiene 3, 5, 6, 8 y 9, y faltan 1, 2, 4 y 7
- Los arreglos
lines y columns se encargan de revisar duplicados en filas y columnas
- Hace falta un nuevo arreglo para revisar duplicados en las celdas
Grid de campos de bits por celda
- El arreglo
cells refleja todo el grid representando cada celda como un entero de 9 bits: var cells = [_]u9{0} ** 9;
- Es más fácil acceder a
cells como una matriz 3x3
- Se llena el arreglo
cell de manera similar a como se hizo con la matriz 9x9
- A partir de la fila y columna del elemento en el grid 9x9 original, hace falta determinar la fila y la columna en la matriz
cell
- Como la división entera es muy lenta, se usa el arreglo
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; para proporcionar el resultado de la división
- Al acceder a la matriz con la fila
i y la columna j del elemento en el grid 9x9, se verifica con una operación AND bit a bit si un número específico ya existe en la celda del elemento: cell[cindx[i]][cindx[j]] & code
- Si el resultado es 0, el número aún no está en la celda; de lo contrario, está duplicado
Prueba de duplicación de elementos
- La verificación de duplicados del elemento se completa combinando con OR bit a bit (
|) todos los elementos previos de la misma fila, columna y celda, y luego aplicando AND bit a bit con el code del elemento
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- Si el resultado es 0, el elemento aún no existe en la fila, columna o celda
- Si el resultado no es 0, el programa se detiene ejecutando la instrucción
unreachable
- Es la forma más simple de representar explícitamente un error en tiempo de ejecución en Zig
- El código real también imprime detalles sobre la ubicación del error
- Ejemplo: si se reemplaza el
0 justo después del primer 8 de la cadena de entrada por 5, ocurre un error porque ya hay un 5 en la fila 3, columna 1
Actualización de estructuras de datos
- En la función
set, un bucle for doble interactúa fila por fila para copiar cada nuevo elemento de la cadena de entrada s al grid
- La variable
k mantiene el índice del nuevo carácter de entrada en la cadena s
- Al restar
'0', el carácter se convierte a u4 (variable c)
- Si el nuevo elemento a insertar en el grid no es 0 (
c != 0), el code calculado con la instrucción de desplazamiento a la izquierda se copia a cada grid espejo
- Se aplica OR bit a bit (
|=) con el grid espejo correspondiente:
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
- No hace falta probar explícitamente si
c está entre 1 y 9, porque se producirá un desbordamiento al ejecutar la operación de desplazamiento
- Ejemplo: si se reemplaza el
0 justo después del primer 8 de la cadena de entrada por :, se produce un error en tiempo de ejecución
- Si ese mismo
0 se reemplaza por /, ocurre un error similar en tiempo de ejecución
- El programa solo funciona cuando los valores están entre 1 y 9, es decir, cuando el grid de entrada contiene únicamente dígitos decimales
- Como muchos grids de Sudoku en la web representan
0 con ., en la función set existe la línea if (s[k] == '.') c = 0;
- Esto evita convenientemente la operación de desplazamiento, ya que el valor de
c es 0
Prototipado y robustez
- Los errores forzados de las dos secciones anteriores muestran una función importante de Zig
- Una de ellas es la robustez de Zig: en el caso de la operación de desplazamiento, no se permite un comportamiento incorrecto y este se detecta en tiempo de ejecución
- Aunque todo parece orientado a la eficiencia, es un caso típico en el que el rendimiento se intercambia por robustez
- En C, si una operación de desplazamiento pierde bits, es problema del programador, y eso se traduce en mejor rendimiento de ciertas instrucciones de ensamblador
- La otra función es la posibilidad de usar bloques de prueba para prototipado
- Las posibilidades de aplicación son innumerables, y la aplicación mostrada es solo depurar una situación específica cuando ocurre un error
- Solo estas funciones ya ofrecen capacidades sorprendentes, muy poco comunes en un lenguaje de programación, especialmente en un lenguaje compilado
Conclusión
- Zig se compone de tres elementos clave: compatibilidad con C, compilación cruzada e instalación simple
- Estas características muestran su potencial para convertirse en un nuevo estándar de los lenguajes de programación de sistemas
- Muchas ventajas que antes solo se encontraban en lenguajes interpretados están migrando gradualmente a lenguajes compilados para ofrecer mejor rendimiento
- En Zig, esa similitud con los lenguajes interpretados destaca especialmente por el concepto de ejecución en tiempo de compilación
- Esto hace que Zig sea especialmente diferente y poderoso, aunque también más difícil de entender
1 comentarios
Opiniones de Hacker News
Este artículo al principio afirma que “Zig no es solo un lenguaje simple, sino una forma completamente nueva de programar”, pero en realidad casi no habla de funciones realmente propias de Zig
La inferencia de tipos, las structs anónimas,
labeled breaky demás ya existían en otros lenguajes desde hace muchoLo verdaderamente único es comptime, pero esa parte ni siquiera se menciona
No es un concepto totalmente nuevo como los macros de Lisp, pero resulta interesante la forma en que Zig lo usa en lugar de genéricos
Aun así, la afirmación del artículo se siente bastante exagerada
Rust permite expresar con claridad el momento en que se ejecuta el código, y es impresionante su diseño tipo motor de consultas que recorre todo el espacio del código
Ver la documentación de D
Si es una
const-expression, se ejecuta automáticamenteSon lenguajes completamente distintos, como Java y Scala
comptimeno es tanto un invento mágico como una versión moderna de la metaprogramaciónZig es más limpio que las plantillas de C++, pero se siente más como una alternativa práctica que como algo revolucionario
Personalmente, no entiendo el entusiasmo excesivo, igual que pasó con Rust
Leí toda la documentación de Zig y me desconcertó no encontrar nada especialmente sorprendente
El mayor problema de Zig es que no puedes adjuntar datos a los errores
Los errores solo se transmiten por un canal secundario, lo que dificulta el debugging, y al final los desarrolladores terminan omitiendo los datos del error
Ver este issue relacionado
Con un código simple como
AccessDeniedes difícil saber cuál fue la causaEn la práctica, incluso usando objetos
Errorcomplejos, muchas veces hace falta un canal de diagnóstico apartePor el overhead de rendimiento o por problemas del estado del sistema, según el caso puede ser más seguro resolverlo con binding diferido
Zig tiene una filosofía que prioriza esta precisión y determinismo
Ver este issue relacionado
Pero lo que de verdad hace falta es logging estructurado y rastreo de contexto basado en la pila de llamadas
std.zonsuele ponerse como buen ejemplo, y en la comunidad hay movimiento para reunir distintos patrones de manejo de errores e incorporarlos al estándarPuede evitar que desarrolladores perezosos simplemente les peguen datos por encima sin pensar
Estoy de acuerdo con la idea de que la forma de desarrollar Zig en sí misma es una nueva manera de desarrollar un lenguaje
Impresiona ese proceso de evolución lenta en el que revisan cuidadosamente las funciones y eliminan lo innecesario
Me gustaría escuchar con más detalle qué tiene Zig de realmente distintivo
Me gusta que Zig se pueda instalar desde PyPI
El paquete ziglang se puede instalar con
pip install ziglangy usar de inmediatoIncluso se puede compilar código C con
uvxDa un poco de pena que presenten como “innovación” de Zig funciones que ya existían en lenguajes como Ada, Object Pascal o Modula-2
Es interesante cómo, al volver a empaquetarlas con sintaxis estilo C, ideas de hace 40 años parecen nuevas otra vez
La introducción del artículo estaba bien, pero después solo se queda en una enumeración de funciones de Zig
Su sintaxis intuitiva y su flujo de control explícito (
defer, etc.) sí resultan atractivosGracias a
comptime, tampoco hace falta aprender una sintaxis separada para macrosTodo encaja de forma natural, y aunque lo uses por primera vez, se siente como una herramienta que ya conocías desde hace tiempo
La sintaxis
for (0..9)de Zig es intuitiva, pero al ser un intervalo abierto, a veces confundeComo con
range(0, 9)de Python, es fácil olvidar si incluye o no el último valor0..9y0..=9El tamaño del intervalo se calcula simplemente por diferencia, y recorrerlo en reversa también se vuelve sencillo
0..<5(abierto) y0...5(cerrado)No me gusta la regla de identificadores de Zig
Se siente raro mezclar
snake_caseycamelCaseAun así, su sistema de build, su allocator, y la experiencia de compilación son excelentes
Uso Rust principalmente, pero sigo teniendo curiosidad por Zig
Lo mismo con la regla de prefijos de las bibliotecas en C, también me resulta molesta
El atractivo de Zig no está en una sola función, sino en la acumulación de decisiones prácticas
Decisiones que al principio parecían radicales se vuelven comprensibles conforme profundizas más
Zig es un lenguaje que recompensa a los desarrolladores curiosos
Una de las razones por las que Zig es bueno es que reconoce la realidad del código de sistemas de bajo nivel
Muchos lenguajes ignoran estas cosas por motivos estéticos, pero Zig no lo hace
Ver la documentación de
page_allocator