HeadlinesBriefing favicon HeadlinesBriefing.com

Parse Don't Validate en Rust: Vectores no vacíos

Hacker News •
×

Al igual que muchos programadores, encuentro fascinante el artículo de Alexis King sobre Parse, don't validate, porque da un nombre a un patrón que me parece familiar e importante, uno que he observado y utilizado en el pasado sin nombrarlo explícitamente. Este artículo es una revisión del patrón "Parse, don't validate" aplicado al lenguaje de programación Rust (el artículo original utiliza Haskell). Me interesó especialmente encontrar ejemplos educativos de este patrón en la biblioteca estándar de Rust y en otros proyectos conocidos. Sin repetir el artículo original (¡por favor, léelo primero!), aquí está su esencia. Consideremos el venerable Vec; su primer método devuelve Option<&T>. ¿Por qué? Porque un vector no está garantizado a tener elementos, ¿qué hacer si first se invoca en uno vacío? Devolver un Option en este caso es idiomático en Rust [1], con una sintaxis conveniente para aceptar el resultado de funciones que devuelven Option y decidir qué hacer a continuación. Entonces, ¿cuál es el problema? Imagina que tenemos una función para leer algunas rutas de configuración desde una variable de entorno, mientras que imponemos la invariante de que la lista no puede estar vacía: use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "empty CONFIG_DIRS"); Ok(directories) } Hasta aquí, todo bien. Ahora veamos un uso típico de esta función: fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } Una vez que get_configuration_directories devuelve un resultado exitoso, estamos garantizados de que el vector no está vacío. Y sin embargo, si queremos obtener el primer elemento de este vector, tenemos que usar el método first que devuelve Option<&T>. Por lo tanto, estamos obligados - de nuevo - a manejar un caso potencialmente vacío (donde la opción es None). Como afirma el artículo original, esto tiene varios problemas con la claridad del código, posibles implicaciones de rendimiento y una bomba de tiempo si la invariante alguna vez cambia en get_configuration_directories. El problema central es que Vec es fundamentalmente un tipo que puede estar vacío; podemos llevar un comentario de "Este no puede estar vacío, lo prometo" en todo el código relevante, pero no está formalmente verificado por nada. Un tipo para un vector "no vacío" La solución es aprovechar el sistema de tipos para imponer una nueva invariante establecida. Podemos usar un tipo separado para "un vector que no puede estar vacío"; de hecho, tales tipos ya existen en varios crates de Rust - por ejemplo nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } Este tipo no tiene un constructor que permita "no elementos"; su new toma un elemento, y su first devuelve &T sin un Option: pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } El resto del crate se ocupa de hacer que Non Empty se comporte lo más cerca posible de un Vec normal, implementando muchos traits útiles, así como conversiones como: pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } Veamos cómo se vería nuestra función get_configuration_directories si devolviera un Non Empty en lugar de un Vec simple: fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } Note el uso de Non Empty::from_vec aquí - este es donde se establece la invariante. Ahora...

FAQ P: ¿Cuál es el principal beneficio de usar un tipo de vector no vacío en Rust?

FAQ R: El principal beneficio es que impone la invariante de no vacuidad a nivel de tipo, eliminando la necesidad de comprobaciones en tiempo de ejecución y manejo de Option al acceder al primer elemento, mejorando la claridad y seguridad del código.