el-zen-de-zig

El Zen de Zig: código explícito y sin sorpresas

  • 4 min

El Zen de Zig es una filosofía de diseño explícita y predecible. Cada lenguaje tiene una personalidad: Python valora la legibilidad, C++ valora la retrocompatibilidad y Zig valora que lo que ocurre se vea en el código.

Zig tiene una filosofía muy clara, pragmática y directa. Se centra en la mantenibilidad.

Andrew Kelley, el creador de Zig, parte de una premisa sencilla: pasamos mucho más tiempo leyendo código que escribiéndolo. Por tanto, el lenguaje debe optimizarse para facilitar la lectura y la comprensión, incluso si eso significa ser un poco más verboso al escribir.

Si tienes Zig instalado, puedes ver estos principios escribiendo en tu terminal:

zig zen
Copied!

Vamos a centrarnos en dos principios de esa lista: sin flujo de control oculto y sin asignaciones de memoria ocultas.

Sin flujo de control oculto

En muchos lenguajes modernos, leer una línea de código no te garantiza saber qué va a pasar. Veamos un ejemplo teórico en un lenguaje tipo C++ o C#:

// Ejemplo en C++ / C#
var a = b + c;
Copied!

¿Qué hace esta línea?

  1. ¿Es una simple suma matemática?
  2. ¿O b es un objeto que tiene sobrecargado el operador +, el cual ejecuta una función compleja que tarda 5 segundos y conecta a una base de datos?
  3. ¿Podría esa operación lanzar una excepción y hacer que el programa salte a un bloque catch lejano, interrumpiendo el flujo normal?
  4. ¿Podría a ser una propiedad (setter) que ejecuta lógica al asignarle un valor?

No lo sabes. Para saberlo, tienes que conocer la definición de los tipos, leer la documentación y entender todo el contexto.

En Zig, la regla es estricta: Si no lo ves, no ocurre.

  • No hay sobrecarga de operadores: + siempre es una suma. Si quieres sumar dos vectores, llamas a vectorAdd(a, b). Es más largo de escribir, sí, pero al leerlo sabes exactamente que es una llamada a función.
  • No hay excepciones: El control de errores es explícito a través de valores de retorno. El flujo nunca “salta” por una excepción invisible.
  • No hay propiedades: Acceder a estructura.campo es solo leer memoria. Si quieres lógica, usas estructura.getCampo().

Esta característica hace que el código sea más fácil de revisar. No tienes que mantener en la cabeza un mapa complejo de efectos secundarios invisibles.

Sin asignaciones de memoria ocultas

Este principio resulta especialmente importante en sistemas, motores gráficos y dispositivos embebidos.

En lenguajes de alto nivel (Java, JS, Python), el Garbage Collector asigna y libera memoria por ti. Es cómodo, pero pierdes el control sobre cuándo ocurre (las temidas pausas del GC).

Incluso en C++ o Rust, la librería estándar a menudo asigna memoria “por detrás”. Si usas std::vector en C++ o Vec en Rust y añades elementos, el lenguaje pedirá memoria al sistema operativo (heap) automáticamente.

En Zig, la librería estándar no puede asignar memoria a menos que tú se lo permitas explícitamente.

Si una función necesita memoria dinámica, debe aceptar un parámetro allocator.

// En Zig, la intención es explícita
// Vemos claramente que esta función necesita memoria para funcionar
fn concatenar(allocator: Allocator, a: []u8, b: []u8) ![]u8 {
    ...
}
Copied!

Esto tiene implicaciones profundas:

  1. Sabes qué operaciones pueden reservar memoria: buscando los parámetros allocator, puedes localizar los puntos donde se solicita memoria dinámica.
  2. Tú eliges la estrategia: puedes proporcionar un asignador de propósito general, uno basado en un búfer fijo o una arena para liberar varias reservas de golpe. La función solo usa el asignador recibido.

Para sistemas embebidos (como Arduino o Raspberry Pi Pico), esto es crucial. Puedes escribir código que garantice cero asignaciones dinámicas, evitando fragmentación de memoria y cuelgues inesperados.

Comunicar la intención con precisión

El Zen de Zig nos dice: “Communicate intent precisely” (Comunica la intención con precisión).

A veces, Zig puede parecer un poco “pedante”. Te obliga a manejar cada posible error. Te obliga a decidir qué tipo de entero usar (u8, i32, usize) en lugar de usar un int genérico. Te obliga a pasar el asignador.

Ese esfuerzo extra al principio suele compensar después. El resultado es un software:

  • Robusto: Has tenido que pensar en los casos borde.
  • Rápido: No hay ciclos de CPU desperdiciados en trabajo oculto.
  • Portable: Al no depender de un entorno de ejecución pesado, tu código corre igual en un superordenador que en un microcontrolador.