3 puntos por GN⁺ 2024-07-23 | 1 comentarios | Compartir por WhatsApp

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

 
GN⁺ 2024-07-23
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 spec de 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

    • Código de ejemplo:
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • Código de ejemplo:
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • Se recomienda no usar este último en drivers en modo kernel
  • 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