3 puntos por GN⁺ 2026-02-23 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2026-02-23
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 newtype no 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”

    • Incluso el enfoque de “parse, don’t validate” basado en newtype sigue siendo muy útil en la práctica
      Cuando 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..100 o 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 vector
    • El objetivo final es el ‘correct by construction’
      Tipos como NonZeroU32 son 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án
      Así, la carga de depuración se mueve del tiempo de ejecución al momento del diseño
    • También se puede buscar material relacionado con la frase “make invalid states impossible/unrepresentable”
      Como ejemplo, vale la pena revisar "Domain Modeling Made Functional" y este video relacionado
    • El ejemplo de dividir entre cero es un caso donde la separación de responsabilidades está mal planteada
      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 arreglo
    Los tipos Vect n a y Fin n de Idris son un ejemplo de eso

    • En Rust también hay bibliotecas que imitan tipos dependientes mediante macros
      Ejemplo: anodized (video de presentación)
    • Si la longitud del arreglo se lee desde stdin, entonces no se puede conocer en tiempo de compilación, así que esta validación queda limitada a los casos con información estática
    • Ojalá este tipo de funcionalidad se vuelva más comú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

    • Hay una tensión entre la frase de Perlis de “100 funciones sobre una sola estructura de datos” y “Parse, Don’t Validate”
      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
    • Esto no es tanto una alternativa pura como un trade-off
      La entrada externa al final siempre hay que parsearla, así que no la reemplaza por completo
    • Suena parecido a la crítica de “stringly typed language”, pero en realidad es un proceso de refinar gradualmente la forma de los datos
    • El equilibrio es importante
      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> o Number<char> lanzan una excepción cuando el valor se sale del rango

  • El ejemplo try_roots del artículo en realidad es un contraejemplo
    Expresar en Rust con tipos la restricción b^2 - 4ac >= 0 se vuelve muy complejo
    En casos así, simplemente devolver Option y validar dentro de la función es más razonable
    La mayoría de las validaciones tratan la interacción entre varios valores, por lo que resolverlo con “parsing” resulta incómodo

    • Cuando la validez de la entrada depende de la relación entre varios argumentos, al final hay que combinarlo en una forma como 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

    • En Go también se intenta, pero se vuelve algo verboso por el abuso de punteros y la ausencia de tipos algebraicos
  • 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

    • Hubo una pregunta sobre qué tooling usan
  • 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álidos
    C# también quedará mucho más limpio si en el futuro incorpora DU nativas