Parsea, no valides
La esencia del diseño guiado por tipos
- Un eslogan simple para explicar el diseño guiado por tipos (
type-driven design): parsea, no valides
- Este eslogan se refiere a una forma de aprovechar el sistema de tipos para aumentar la seguridad y la corrección del código
El espacio de posibilidades
- Un sistema de tipos estático permite determinar con facilidad si cierta función puede implementarse o no
- Ejemplo:
foo :: Integer -> Void no puede implementarse (el tipo Void no puede contener valores)
- Ejemplo: la función
head :: [a] -> a no está definida cuando la lista está vacía
Convertir funciones parciales en funciones totales
Manejo de expectativas
- Como la función
head no puede devolver un valor cuando la lista está vacía, se puede usar el tipo Maybe para que pueda devolver Nothing
- Sin embargo, esto puede resultar incómodo al usarla
Transmitir expectativas
- Usar el tipo
NonEmpty para representar listas no vacías permite garantizar que la función head siempre devolverá un valor
- Al usar el tipo
NonEmpty, se eliminan verificaciones innecesarias y los errores pueden detectarse en tiempo de compilación mediante el sistema de tipos
El poder del parseo
- La diferencia entre parsear y validar está en cómo se preserva la información
- La función
validateNonEmpty verifica que una lista no esté vacía, pero no preserva esa información
- La función
parseNonEmpty verifica que una lista no esté vacía y preserva la información mediante el tipo NonEmpty
Los riesgos de la validación
- Un enfoque basado en validación puede provocar un problema llamado "shotgun parsing"
- Esto puede llevar a situaciones en las que el programa procesa parte de la entrada y luego descubre que el resto no es válido
- Parsear divide el programa en dos etapas: en la primera se verifica la validez de la entrada, y en la segunda solo se procesa entrada válida
El parseo en la práctica
- Centrarse en los tipos de datos y hacer que la firma de tipos de las funciones sea lo más específica posible
- Usar estructuras de datos que no puedan representar estados ilegales y convertir los datos a representaciones concretas lo antes posible
- Dejar que los tipos de datos guíen el código, en lugar de que el código controle los tipos de datos
- Las funciones que devuelven
m () deben usarse con cuidado
- No hay que temer parsear datos en varias etapas
- Evitar representaciones desnormalizadas de los datos y, cuando sea necesario, gestionarlas mediante encapsulación
- Deben usarse tipos de datos abstractos que hagan que los validadores parezcan parsers
Resumen, reflexión y lecturas relacionadas
- Aprovechar al máximo el sistema de tipos de Haskell no es difícil y no requiere usar extensiones modernas del lenguaje
- La idea central es "escribir funciones totales", algo simple en teoría, pero que puede ser difícil de poner en práctica
- Como lecturas relacionadas, se recomiendan la entrada de blog de Matt Parson "Type Safety Back and Forth" y el artículo de Matt Noonan "Ghosts of Departed Proofs"
Resumen de GN⁺
- Este artículo explica cómo usar el sistema de tipos de Haskell para mejorar la seguridad y la corrección del código
- Destaca la importancia de entender la diferencia entre parsear y validar, y de comprobar la validez de la entrada mediante el parseo
- Es importante usar estructuras de datos que no puedan representar estados ilegales y convertir los datos a representaciones concretas lo antes posible aprovechando el sistema de tipos
- Como lecturas relacionadas, se recomiendan la entrada de blog de Matt Parson y el artículo de Matt Noonan
1 comentarios
Opiniones de Hacker News
Este consejo y el artículo son muy útiles
También es útil para quienes no usan lenguajes funcionales con tipado estático
Esta idea trasciende paradigmas
Se pueden encontrar conceptos similares en la literatura orientada a objetos de los 80 y 90, por ejemplo, Design by Contract
TypeScript suele escribirse de una manera que refina tipos en tiempo de ejecución
Es probable que Design by Contract haya influido en
specde Clojure (Clojure es un lenguaje dinámico)Básicamente, esto trata sobre supuestos y garantías (requerir y proveer)
Una vez que los supuestos se verifican y las garantías se cumplen, no hace falta volver a comprobar supuestos duplicados en otras partes del programa
Puede resultar confuso ver que en el código se vuelven a comprobar propiedades que ya estaban garantizadas, y eso dificulta entenderlo y mejorarlo
Este patrón también funciona bien en C# moderno y además ahorra espacio
Es bueno aprovechar un sistema de tipos fuerte para hacer que los casos de error no puedan expresarse, lo que ayuda a reducir bugs de software
Lleva más tiempo pensar el problema y seguir el diseño, pero en muchos casos ese tiempo vale la pena
El eslogan "Parse, don’t validate" resume bien el diseño basado en tipos
Personalmente, me parece mejor "realizar siempre la validación solo en un único constructor"; así, los objetos inválidos simplemente no pueden existir
Si hay que modificar un objeto, debería implementarse volviendo a llamar al mismo constructor para construir el nuevo estado
Me recuerda a la sección 5 de qmail, que incluye ideas como "no parsees" y "hay buenas interfaces y hay interfaces de usuario"
Si enseñara un curso intermedio de programación, les pediría a los estudiantes que escribieran un ensayo comparando y contrastando estas propuestas; cada una tiene algo que enseñar y al principio pueden parecer contradictorias
Material relacionado: Richard Feldman, "Making Impossible States Impossible"
Discusiones anteriores:
Enviado a Crowdstrike
Me acordé de un comentario de alguien durante la fiebre del XML de mediados de los 2000: muchas organizaciones eligieron XML porque XML les daba un parser
Aunque escribir un parser no es difícil y hasta es divertido, no entiendo por qué la gente no quiere escribir parsers
Me pregunto si esto contradice la opinión de que la palabra clave "required" de Protocol Buffers fue un gran error
Lo ideal sería contar tanto con parsing flexible y no validado como con funciones de parsing validado