BookinglyTech News
Software

Parse, don't validate en Rust: tipos que eliminan comprobaciones repetidas

Un repaso práctico del patrón aplicado a Rust propone usar tipos como NonEmpty para que el compilador garantice invariantes en vez de repetir comprobaciones en tiempo de ejecución.

2 min de lecturaLobsters0 vistas

El patrón que Alexis King bautizó como parse, don't validate tiene traducción directa a Rust, y no hace falta inventarse ejemplos: aparecen en la biblioteca estándar y en proyectos conocidos como posixutils-rs y rust-analyzer. La idea de fondo es que una función no devuelva un dato ya validado, sino un tipo distinto que solo pueda existir si la comprobación se cumplió. Un repaso reciente del tema aterriza el concepto en código Rust y explica dónde compensa pagar el precio de un tipo nuevo.

El Option que sobra

Vec::first devuelve Option<&T> porque un vector puede estar vacío, y el lenguaje no tiene forma de saber lo contrario. El ejemplo que usa el autor es una función que lee la variable de entorno CONFIG_DIRS, la parte por comas y comprueba con un ensure! que la lista resultante no esté vacía. Devuelve Result<Vec<PathBuf>>, correcto. Pero quien la llama sigue teniendo que hacer match sobre first(): Some(cache_dir) => initialize_cache(cache_dir), y en la rama None, un unreachable! con un comentario que promete que el vector nunca viene vacío. El compilador no sabe lo que sabe el programador, así que la comprobación se repite y queda como una bomba de relojería si alguien cambia la función más adelante.

La alternativa es mover el invariante al sistema de tipos con nonempty, un crate que ofrece NonEmpty<T>, compuesto por un head: T y un tail: Vec<T>. No tiene constructor para cero elementos: new toma uno, singleton construye con uno y first devuelve &T, sin Option de por medio. La conversión desde un Vec vive en from_vec, que devuelve Option<NonEmpty<T>>. Ese es el único punto donde se establece el invariante, y es justo la frontera donde el dato entra al programa.

Con eso, la función de configuración pasa a devolver Result<NonEmpty<PathBuf>> y el cliente llama a first() sin rama de error. Si alguien modifica el parser y deja escapar un vector vacío, no compila.

Dónde se usa de verdad

Posixutils-rs, la reescritura en Rust de las utilidades POSIX básicas, define su estructura Pipeline con commands: NonEmpty<Command>. El parser devuelve None cuando no encuentra ningún comando en lugar de construir una estructura con la lista vacía, de modo que quien consume el AST no repite la comprobación. Es el mismo truco que usa la biblioteca estándar con NonZero para los enteros, que descarta el cero a nivel de tipo.

El otro caso que menciona el texto es rust-analyzer, donde el autor ve un ejemplo de refinado progresivo: un tipo que representa una ruta absoluta del sistema de ficheros y va restringiendo lo que es válido conforme avanza el análisis, en vez de validar todo de golpe al final.

Nada de esto es gratis. Un tipo así obliga a implementar rasgos para que se comporte como un Vec en todo lo demás, y cada frontera del programa necesita su conversión. La diferencia es cuándo se paga: una comprobación que vive dentro del tipo se escribe una vez y no vuelve a aparecer en cada punto de uso.