- Explica un enfoque de diseño en Rust que usa el sistema de tipos para garantizar invariantes en tiempo de compilación, en lugar de validarlos en tiempo de ejecución
- Define nuevos tipos (newtype) como
NonZeroF32 y NonEmptyVec para hacer imposible representar estados inválidos (0, vector vacío, etc.)
- En lugar de devolver fallos con
Option o Result, refuerza las restricciones en los argumentos de las funciones para bloquear errores de antemano
- Presenta casos como
String::from_utf8 o serde_json::from_str, donde el parseo convierte datos en tipos con significado
- El principio de diseño de hacer imposibles los estados ilegales y adelantar la validación tanto como sea posible mejora la estabilidad y la legibilidad del código
1. Expresar restricciones con tipos en lugar de validación en tiempo de ejecución
- En la función
divide(a, b), dividir entre 0 provoca un pánico en tiempo de ejecución
- Se puede expresar el fallo devolviendo
Option, pero eso debilita el tipo de retorno
- Se define el tipo
NonZeroF32 para permitir crear solo valores distintos de 0
- Su constructor tiene la forma
fn new(n: f32) -> Option<NonZeroF32>, y devuelve None en caso de fallo
- Si se define como
divide_floats(a: f32, b: NonZeroF32), la validación en tiempo de ejecución deja de ser necesaria
- La responsabilidad de validar se mueve del interior de la función hacia quien la llama, eliminando errores de antemano
2. Eliminar validaciones duplicadas y simplificar el código
- En la función
roots(a, b, c), si la validación de a == 0 se maneja con Option, se termina validando por duplicado tanto en la llamada como dentro de la función
- Con
NonZeroF32, la validación se hace una sola vez y la lógica posterior se simplifica
- Con el mismo principio, se puede definir
NonEmptyVec<T> para no permitir vectores vacíos
- Si
get_cfg_dirs() devuelve NonEmptyVec<PathBuf>, ya no hace falta una validación adicional en main()
3. Casos reales: String y serde_json
String es internamente un nuevo tipo (newtype) de Vec<u8>, y String::from_utf8 realiza la verificación de validez
- Después de eso, se puede usar de forma segura como una cadena con UTF-8 garantizado
serde_json con from_str::<Sample> parsea JSON a una estructura y garantiza en tiempo de compilación la presencia de campos y la consistencia de tipos
- Restricciones como la presencia de los campos
foo y bar, la coincidencia de tipos o la longitud de arreglos se comprueban a nivel de tipos
4. Dos principios del diseño guiado por tipos
- Hacer imposible representar estados ilegales
NonZeroF32 no puede representar 0, y NonEmptyVec no puede representar un estado vacío
- Una función de validación simple como
is_nonzero sigue permitiendo representar estados inválidos, así que es incompleta
- Realizar la validación lo antes posible
- Si la validación queda dispersa por todo el código, como en ‘Shotgun Parsing’, puede derivar en vulnerabilidades de seguridad (como CVE-2016-0752)
- Si todas las restricciones se comprueban en la etapa de parseo, la lógica posterior puede ejecutarse de forma segura
5. Pruebas basadas en tipos y aplicaciones en Rust
- Según la correspondencia Curry-Howard, los tipos pueden verse como proposiciones lógicas y los valores como sus pruebas
- Con el crate
typenum, es posible verificar relaciones matemáticas en tiempo de compilación (3 + 4 = 8)
- El sistema de tipos permite demostrar la corrección del programa en tiempo de compilación
6. Consejos para aplicarlo en la práctica
- Aunque una API externa exija tipos simples como
bool o i32, internamente conviene representarlos como enum o newtype con significado
- Ejemplo: definir
LightBulbState { On, Off } e implementar From<LightBulbState> for bool
- Si existen funciones de validación simples como
verify() o do_something_fallible(), conviene considerar una conversión estructurada de tipos mediante parseo
- Si una función no tiene efectos secundarios, se puede usar
Result<Infallible, MyError> para expresar mediante tipos un estado intencionalmente imposible
7. Conclusión
- Si se usa el sistema de tipos de Rust como herramienta de validación, mejora la claridad y la estabilidad del código
- Varias herramientas del ecosistema Rust, como
Vec, sqlx y bon, ya aprovechan diseños basados en tipos
- No todos los problemas pueden resolverse con tipos, pero llevar la lógica de validación al sistema de tipos mejora la mantenibilidad y la seguridad
- Se recomienda aprovechar al máximo el potente sistema de tipos de Rust para escribir código donde el compilador detecte los errores
1 comentarios
Opiniones de Hacker News
El ejemplo de dividir entre cero usado en este artículo no es adecuado para explicar el principio de “Parse, Don’t Validate”
La esencia de este principio está en funciones que convierten datos no confiables en tipos estructuralmente correctos
En el artículo de Alexis King "Names are not type safety", también se aclara que el patrón
newtypeno garantiza por completo el “correct by construction”Cuando el sistema de tipos no puede expresar directamente las invariantes, un enfoque realista es usar tipos abstractos que imiten un parser mediante un smart constructor
El segundo ejemplo, el de non-empty vec, es un caso mucho mejor, porque garantiza dentro del sistema de tipos que “siempre existe al menos un elemento”
newtypesigue siendo muy útil en la prácticaCuando no se sabe de dónde vino una cadena, un valor encapsulado aumenta mucho la confiabilidad
Para lograr un correctness-by-construction completo haría falta un sistema de tipos dependientes, pero también existen alternativas ligeras como los pattern types de Rust
Por ejemplo, se puede restringir un rango con
i8 is 0..100o expresar un slice no vacío con[T] is [_, ..]Aun así, una non-empty list con forma de
(T, Vec<T>)muestra el choque entre la practicidad y la pureza teórica, ya que tiene muchas limitaciones para tratarla como un vectorTipos como
NonZeroU32son simples, pero la verdadera fuerza está en diseñar toda la lógica de dominio con tipos para que el compilador actúe como guardiánAsí, la carga de depuración se mueve del tiempo de ejecución al momento del diseño
Como ejemplo, vale la pena revisar "Domain Modeling Made Functional" y este video relacionado
En vez de intentar envolverlo a ese nivel, sería más claro envolver el comportamiento de funciones aritméticas como el overflow
Organicé enlaces a discusiones recientes relacionadas
Parse, Don't Validate (2019) (febrero de 2026, 172 comentarios)
Parse, Don’t Validate – Some C Safety Tips (julio de 2025, 73 comentarios)
Parse, Don't Validate (2019) (julio de 2024, 102 comentarios), entre otros
Solo lo compartí como referencia
El enfoque de parsing over validation tiene límites cuando no se pueden conocer todos los casos del mundo real
Hacer que falle lo antes posible, como en un formato de archivo, está bien, pero hay que tener cuidado al aplicarlo a lógica de negocio o al modelado de transiciones de estado
Si cambian los requisitos reales, el sistema puede dejar de acomodarlos y al final los usuarios terminan rodeándolo con atajos
En otros lenguajes se puede ir más lejos con tipos dependientes
Por ejemplo,
get_elem_at_index(array, index)puede garantizar en tiempo de compilación que el índice está dentro del rango, incluso sin conocer de antemano la longitud del arregloLos tipos
Vect n ayFin nde Idris son un ejemplo de esoEjemplo: anodized (video de presentación)
También existe el enfoque de poner varias funciones sobre un solo tipo
Es la idea de representar todos los datos con un solo mapa, como en Clojure, y permitir que toda la biblioteca estándar lo manipule
Las invariantes importantes pueden meterse en los tipos, o pueden expresarse con funciones simples
Incluso en lenguajes de tipado dinámico hay hábitos de diseño que producen un efecto parecido
La entrada externa al final siempre hay que parsearla, así que no la reemplaza por completo
En sistemas de tipos estructurales se puede imitar el tipado nominal con branding, y también al revés, pero no es ergonómico
Al final, lo realista es mezclar ambos enfoques de manera adecuada
Esta discusión recuerda a la funcionalidad de concepts en C++
En Concept-based Generic Programming de Bjarne Stroustrup se muestra un ejemplo que valida automáticamente conversiones enteras
Tipos como
Number<unsigned int>oNumber<char>lanzan una excepción cuando el valor se sale del rangoEl ejemplo
try_rootsdel artículo en realidad es un contraejemploExpresar en Rust con tipos la restricción
b^2 - 4ac >= 0se vuelve muy complejoEn casos así, simplemente devolver
Optiony validar dentro de la función es más razonableLa mayoría de las validaciones tratan la interacción entre varios valores, por lo que resolverlo con “parsing” resulta incómodo
fn(abc: ValidABC)Este patrón también encaja muy bien en el diseño de APIs
En vez de validar una solicitud JSON, si desde el inicio se parsea a una struct con tipos garantizados, ya no hacen falta validaciones duplicadas en la lógica posterior
En Rust se puede implementar fácilmente con la combinación de serde + custom deserializer
De hecho, vi un caso donde este enfoque redujo en 60% el código de manejo de errores
La misma filosofía también se aplica a los sistemas de diseño de UI
En vez de revisar CSS más tarde, se definen tipos que solo permiten ubicar elementos en unidades de rejilla, de modo que márgenes arbitrarios como 13px se conviertan en errores de compilación
Así el diseño se mantiene determinista
Los records + pattern matching de C# se acercan a este enfoque
Las discriminated unions de F# son más potentes y permiten, con
Result<'T,'Error>, hacer imposibles los estados inválidosC# también quedará mucho más limpio si en el futuro incorpora DU nativas