¿Cuál es una buena manera de diseñar / estructurar grandes programas funcionales, especialmente en Haskell?
He pasado por un montón de tutoriales (Write Yourself a Scheme es mi favorito, con Real World Haskell en segundo lugar), pero la mayoría de los programas son relativamente pequeños y de un solo propósito. Además, no considero que algunos de ellos sean particularmente elegantes (por ejemplo, las vastas tablas de búsqueda en WYAS).
Ahora quiero escribir programas más grandes, con más partes móviles: adquirir datos de una variedad de fuentes diferentes, limpiarlos, procesarlos de varias maneras, mostrarlos en las interfaces de usuario, persistir, comunicarse a través de redes, etc. ¿Cómo podría ¿Cuál es la mejor estructura para que ese código sea legible, mantenible y adaptable a los requisitos cambiantes?
Existe una literatura bastante extensa que aborda estas preguntas para grandes programas imperativos orientados a objetos. Ideas como MVC, patrones de diseño, etc. son recetas decentes para alcanzar objetivos amplios como la separación de preocupaciones y la reutilización en un estilo OO. Además, los nuevos lenguajes imperativos se prestan a un estilo de refactorización de "diseño a medida que creces" para el que, en mi opinión de principiante, Haskell parece menos adecuado.
¿Existe una literatura equivalente para Haskell? ¿Cómo se emplea mejor el zoológico de estructuras de control exóticas en programación funcional (mónadas, flechas, aplicativo, etc.) para este propósito? ¿Qué mejores prácticas podrías recomendar?
¡Gracias!
EDITAR (esto es un seguimiento de la respuesta de Don Stewart):
@dons mencionó: "Las mónadas capturan diseños arquitectónicos clave en tipos".
Supongo que mi pregunta es: ¿cómo debería uno pensar en los diseños arquitectónicos clave en un lenguaje funcional puro?
Considere el ejemplo de varios flujos de datos y varios pasos de procesamiento. Puedo escribir analizadores modulares para los flujos de datos en un conjunto de estructuras de datos, y puedo implementar cada paso de procesamiento como una función pura. Los pasos de procesamiento necesarios para una pieza de datos dependerán de su valor y de los demás. Algunos de los pasos deben ser seguidos por efectos secundarios como actualizaciones de GUI o consultas de bases de datos.
¿Cuál es la forma 'correcta' de vincular los datos y los pasos de análisis de una manera agradable? Se podría escribir una gran función que haga lo correcto para los distintos tipos de datos. O se podría usar una mónada para realizar un seguimiento de lo que se ha procesado hasta ahora y hacer que cada paso de procesamiento obtenga lo que necesite a continuación del estado de la mónada. O uno podría escribir programas en gran parte separados y enviar mensajes (no me gusta mucho esta opción).
Las diapositivas que vinculó tienen una viñeta de Cosas que necesitamos: "Modismos para mapear el diseño en tipos / funciones / clases / mónadas". ¿Cuáles son los modismos? :)
Respuestas:
Hablo un poco sobre esto en Ingeniería de Grandes Proyectos en Haskell y en el Diseño e Implementación de XMonad. La ingeniería en general se trata de gestionar la complejidad. Los principales mecanismos de estructuración de código en Haskell para gestionar la complejidad son:
El sistema de tipos
El perfilador
Pureza
Pruebas
Mónadas para estructurar
Clases de tipos y tipos existenciales
Concurrencia y paralelismo
par
en tu programa para vencer a la competencia con un paralelismo fácil y componible.Refactor
Use el FFI sabiamente
Meta programación
Empaque y distribución
Advertencias
-Wall
para mantener su código limpio de olores. También puede mirar a Agda, Isabelle o Catch para obtener más seguridad. Para una comprobación similar a la pelusa, vea la gran pista , que sugerirá mejoras.Con todas estas herramientas, puede controlar la complejidad y eliminar la mayor cantidad posible de interacciones entre los componentes. Idealmente, tiene una base muy grande de código puro, que es realmente fácil de mantener, ya que es compositivo. Eso no siempre es posible, pero vale la pena apuntar.
En general: descomponga las unidades lógicas de su sistema en los componentes referencialmente más pequeños posibles, luego impleméntelos en módulos. Los entornos globales o locales para conjuntos de componentes (o componentes internos) pueden asignarse a mónadas. Utilice los tipos de datos algebraicos para describir las estructuras de datos centrales. Comparta esas definiciones ampliamente.
fuente
Don le dio la mayoría de los detalles anteriores, pero aquí están mis dos centavos por hacer programas con estado realmente meticuloso como demonios del sistema en Haskell.
Al final, vives en una pila de transformadores de mónada. En la parte inferior está IO. Por encima de eso, cada módulo principal (en el sentido abstracto, no en el sentido del módulo en un archivo) asigna su estado necesario en una capa en esa pila. Entonces, si tiene el código de conexión de su base de datos oculto en un módulo, lo escribe todo sobre un tipo de conexión MonadReader m => ... -> m ... y luego las funciones de su base de datos siempre pueden obtener su conexión sin funciones de otros módulos que deben ser conscientes de su existencia. Puede terminar con una capa que lleva su conexión de base de datos, otra su configuración, un tercio de sus diversos semáforos y mvars para la resolución de paralelismo y sincronización, otro que maneja su archivo de registro, etc.
Primero descubra su manejo de errores . La mayor debilidad en este momento para Haskell en sistemas más grandes es la gran cantidad de métodos de manejo de errores, incluidos los pésimos como Maybe (que está mal porque no puedes devolver ninguna información sobre lo que salió mal; siempre usa Either en lugar de Maybe a menos que realmente solo significa valores perdidos). Averigüe cómo lo va a hacer primero, y configure los adaptadores de los diversos mecanismos de manejo de errores que sus bibliotecas y otros códigos utilizan en su versión final. Esto te ahorrará un mundo de dolor más tarde.
Anexo (extraído de los comentarios; gracias a Lii & liminalisht ):
más discusión sobre las diferentes formas de dividir un gran programa en mónadas en una pila:
Ben Kolera da una gran introducción práctica a este tema, y Brian Hurt analiza soluciones al problema de
lift
incorporar acciones monádicas en su mónada personalizada. George Wilson muestra cómo usarmtl
para escribir código que funcione con cualquier mónada que implemente las clases de tipos requeridas, en lugar de su tipo de mónada personalizado. Carlo Hamalainen ha escrito algunas notas breves y útiles que resumen la charla de George.fuente
lift
incorporar acciones monádicas en su mónada personalizada. George Wilson muestra cómo usarmtl
para escribir código que funcione con cualquier mónada que implemente las clases de tipos requeridas, en lugar de su tipo de mónada personalizado. Carlo Hamalainen ha escrito algunas notas breves y útiles que resumen la charla de George.Diseñar programas grandes en Haskell no es tan diferente de hacerlo en otros idiomas. La programación en general se trata de dividir su problema en piezas manejables y cómo encajarlas; El lenguaje de implementación es menos importante.
Dicho esto, en un diseño grande, es bueno probar y aprovechar el sistema de tipos para asegurarse de que solo pueda unir sus piezas de la manera correcta. Esto podría implicar tipos nuevos o fantasmas para hacer que las cosas que parecen tener el mismo tipo sean diferentes.
Cuando se trata de refactorizar el código a medida que avanza, la pureza es una gran bendición, así que trate de mantener puro el mayor código posible. El código puro es fácil de refactorizar, ya que no tiene interacción oculta con otras partes de su programa.
fuente
Aprendí programación funcional estructurada la primera vez con este libro . Puede que no sea exactamente lo que está buscando, pero para los principiantes en programación funcional, este puede ser uno de los mejores primeros pasos para aprender a estructurar programas funcionales, independientemente de la escala. En todos los niveles de abstracción, el diseño siempre debe tener estructuras claramente organizadas.
El arte de la programación funcional
http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/
fuente
where beginner=do write $ tutorials `about` Monads
)Actualmente estoy escribiendo un libro con el título "Diseño funcional y arquitectura". Le proporciona un conjunto completo de técnicas para construir una gran aplicación utilizando un enfoque funcional puro. Describe muchos patrones e ideas funcionales mientras construye una aplicación similar a SCADA 'Andromeda' para controlar naves espaciales desde cero. Mi idioma principal es Haskell. El libro cubre:
Puede familiarizarse con el código del libro aquí , y el código del proyecto 'Andromeda' .
Espero terminar este libro a fines de 2017. Hasta que eso suceda, puede leer mi artículo "Diseño y arquitectura en programación funcional" (Rus) aquí .
ACTUALIZAR
Compartí mi libro en línea (primeros 5 capítulos). Ver publicación en Reddit
fuente
La publicación del blog de Gabriel Las arquitecturas de programas escalables pueden merecer una mención.
A menudo me sorprende que una arquitectura aparentemente elegante a menudo tiende a caerse de las bibliotecas que exhiben este agradable sentido de homogeneidad, de una manera ascendente. En Haskell esto es especialmente evidente - patrones que tradicionalmente se considerarían "arquitectura de arriba hacia abajo" tienden a ser capturados en las bibliotecas como MVC , Netwire y Nube Haskell . Es decir, espero que esta respuesta no se interprete como un intento de reemplazar a cualquiera de los otros en este hilo, solo que las opciones estructurales pueden y deberían idealmente ser abstraídas en las bibliotecas por expertos en dominios. La verdadera dificultad en la construcción de sistemas grandes, en mi opinión, es evaluar estas bibliotecas en su "bondad" arquitectónica frente a todas sus preocupaciones pragmáticas.
Como liminalisht menciona en los comentarios, El patrón de diseño de categoría es otra publicación de Gabriel sobre el tema, en una línea similar.
fuente
El documento " Teaching Software Architecture Using Haskell " (pdf) de Alejandro Serrano me pareció útil para pensar sobre la estructura a gran escala en Haskell.
fuente
Quizás, en primer lugar, tenga que retroceder y pensar cómo traducir la descripción del problema a un diseño. Dado que Haskell tiene un nivel tan alto, puede capturar la descripción del problema en forma de estructuras de datos, las acciones como procedimientos y la transformación pura como funciones. Entonces tienes un diseño. El desarrollo comienza cuando compila este código y encuentra errores concretos sobre campos faltantes, instancias faltantes y transformadores monádicos faltantes en su código, porque, por ejemplo, realiza un acceso a la base de datos desde una biblioteca que necesita una determinada mónada de estado dentro de un procedimiento de E / S. Y listo, ahí está el programa. El compilador alimenta sus bocetos mentales y le da coherencia al diseño y al desarrollo.
De esta manera, se beneficia de la ayuda de Haskell desde el principio, y la codificación es natural. No me gustaría hacer algo "funcional" o "puro" o lo suficientemente general si lo que tienes en mente es un problema ordinario concreto. Creo que el exceso de ingeniería es lo más peligroso en TI. Las cosas son diferentes cuando el problema es crear una biblioteca que abstraiga un conjunto de problemas relacionados.
fuente