opciones - programa en haskell
¿Diseño a gran escala en Haskell? (8)
Actualmente estoy escribiendo un libro con el título "Diseño funcional y arquitectura". Le proporciona un conjunto completo de técnicas para crear 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 las naves espaciales desde cero. Mi idioma principal es Haskell. El libro cubre:
- Aproximaciones al modelado arquitectónico mediante diagramas;
- Análisis de requerimientos;
- Modelado de dominios DSL incrustados;
- Diseño e implementación de DSL externo;
- Mónadas como subsistemas con efectos;
- Mónadas libres como interfaces funcionales;
- EDSLs con flecha;
- Inversión de control usando eDSLs monádicos libres;
- Software de Memoria Transaccional;
- Lentes;
- Estado, lector, escritor, RWS, mónadas ST;
- Estado impuro: IORef, MVar, STM;
- Modelado de dominios multiproceso y concurrente;
- GUI;
- Aplicabilidad de las principales técnicas y enfoques como UML, SOLID, GRASP;
- Interacción con subsistemas impuros.
Puede familiarizarse con el código del libro here y el código del proyecto ''Andromeda'' .
Espero terminar este libro a finales de 2017. Hasta que eso suceda, puede leer mi artículo "Diseño y arquitectura en programación funcional" (Rus) here .
ACTUALIZAR
Compartí mi libro en línea (primeros 5 capítulos). Ver post en reddit
¿Cuál es una buena manera de diseñar / estructurar grandes programas funcionales, especialmente en Haskell?
He seguido 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 interfaces de usuario, persistirlos, comunicarse a través de redes, etc. ¿Cómo podría? ¿Una mejor estructura para que dicho código sea legible, fácil de mantener y adaptable a los requisitos cambiantes?
Existe una gran cantidad de literatura que aborda estas preguntas para grandes programas imperativos orientados a objetos. Ideas como MVC, patrones de diseño, etc. son prescripciones decentes para alcanzar objetivos amplios como la separación de preocupaciones y la reutilización en un estilo OO. Además, los lenguajes imperativos más nuevos se prestan a un estilo de refactorización de "diseño a medida que creces" al cual, en mi opinión de principiante, Haskell parece ser menos adecuado.
¿Hay una literatura equivalente para Haskell? ¿Cómo se emplea el zoológico de estructuras de control exóticas disponibles en la 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 se debe pensar en los diseños arquitectónicos clave en un lenguaje funcional puro?
Considere el ejemplo de varias secuencias 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 requeridos para una pieza de datos dependerán de su valor y el de otros. Algunos de los pasos deben ir seguidos de efectos secundarios como actualizaciones de GUI o consultas de base 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 función grande que haga lo correcto para los distintos tipos de datos. O uno podría usar una mónada para realizar un seguimiento de lo que se ha procesado hasta el momento y hacer que cada paso del proceso obtenga lo que necesite después del estado de la mónada. O uno podría escribir programas separados en gran medida y enviar mensajes (no me gusta mucho esta opción).
Las diapositivas que vinculó tienen una viñeta de Cosas que necesitamos: "Modismos para asignar el diseño a tipos / funciones / clases / mónadas". ¿Cuáles son los modismos? :)
Aprendí la 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 dispuestas.
El Arte de la Programación Funcional.
Diseñar programas grandes en Haskell no es tan diferente de hacerlo en otros idiomas. Programar en grande es dividir su problema en partes 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 puedas unir tus piezas de una manera correcta. Esto podría involucrar tipos de tipo nuevo o fantasma 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 la mayor parte posible del código. El código puro es fácil de refactorizar, porque no tiene interacción oculta con otras partes de su programa.
Don te dio la mayoría de los detalles anteriores, pero aquí están mis dos centavos por haber realizado programas de estado realmente insignificantes como los demonios del sistema en Haskell.
Al final, vives en una pila de transformadores de mónada. En la parte inferior es IO. Más arriba, cada módulo principal (en el sentido abstracto, no el sentido de módulo en un archivo) asigna su estado necesario a una capa en esa pila. Entonces, si tiene el código de conexión de su base de datos oculto en un módulo, escriba todo sobre un tipo MonadReader Connection m => ... -> m ... y luego las funciones de la base de datos siempre pueden obtener su conexión sin funciones de otros Los módulos deben ser conscientes de su existencia. Puede terminar con una capa que lleva su conexión de base de datos, otra su configuración, una tercera sus diversos semáforos y mvars para la resolución de paralelismo y sincronización, otra las manijas de su archivo de registro, etc.
Determine su manejo de errores primero . 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 maliciosos como Maybe (lo cual es incorrecto porque no puede devolver ninguna información sobre lo que salió mal; use siempre Cualquiera en lugar de Maybe a menos que realmente sólo significa valores perdidos). Descubra cómo lo va a hacer primero y configure los adaptadores de los diversos mecanismos de manejo de errores que utilizan sus bibliotecas y otros códigos en su último. Esto te salvará un mundo de dolor más tarde.
Addendum (extraído de los comentarios; gracias a Lii & liminalisht ) -
más discusión sobre diferentes maneras de dividir un programa grande en mónadas en una pila:
Ben Kolera ofrece una excelente introducción práctica a este tema, y Brian Hurt analiza las soluciones al problema de lift
las acciones monádicas a su mónada personalizada. George Wilson muestra cómo usar mtl
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 breves y útiles notas que resumen la charla de George.
Hablo un poco sobre esto en Ingeniería de grandes proyectos en Haskell y en 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
- Utilice el sistema de tipos para imponer abstracciones, simplificando las interacciones.
- Hacer cumplir invariantes clave a través de tipos
- (por ejemplo, que ciertos valores no pueden escapar de cierto alcance)
- Que cierto código no hace IO, no toca el disco.
- Aplique la seguridad: verifique las excepciones (Quizás / Cualquiera), evite mezclar conceptos (Word, Int, Address)
- Las buenas estructuras de datos (como las cremalleras) pueden hacer innecesarias algunas clases de pruebas, ya que descartan, por ejemplo, errores estáticos fuera de límites.
El perfilador
- Proporcione evidencia objetiva de los perfiles de pila y tiempo de su programa.
- El perfil del montón, en particular, es la mejor manera de asegurar que no se use la memoria de forma innecesaria.
Pureza
- Reduce dramáticamente la complejidad eliminando el estado. Código puramente funcional escala, porque es compositivo. Todo lo que necesita es el tipo para determinar cómo usar algún código; no se romperá misteriosamente cuando cambie alguna otra parte del programa.
- Use una gran cantidad de programación estilo "modelo / vista / controlador": analice los datos externos tan pronto como sea posible en estructuras de datos puramente funcionales, opere en esas estructuras, luego, una vez hecho todo el trabajo, render / flush / serialize. Mantiene la mayor parte de su código puro
Pruebas
- QuickCheck + Haskell Code Coverage, para asegurarse de que está probando las cosas que no puede verificar con los tipos.
- GHC + RTS es excelente para ver si estás gastando demasiado tiempo haciendo GC.
- QuickCheck también puede ayudarlo a identificar API limpias y ortogonales para sus módulos. Si las propiedades de su código son difíciles de establecer, probablemente sean demasiado complejas. Sigue refactorizando hasta que tengas un conjunto limpio de propiedades que puedan probar tu código, que se componga bien. Entonces el código probablemente también está bien diseñado.
Mónadas para la estructuración
- Las mónadas capturan diseños arquitectónicos clave en tipos (este código accede al hardware, este código es una sesión de un solo usuario, etc.)
- Por ejemplo, la X mónada en xmonad, captura con precisión el diseño de qué estado es visible para qué componentes del sistema.
Clases de tipos y tipos existenciales.
- Utilice clases de tipo para proporcionar abstracción: oculte las implementaciones detrás de las interfaces polimórficas.
Concurrencia y paralelismo
- Entra en tu programa para vencer a la competencia con un paralelismo fácil de componer.
Refactor
- Puedes refactorizar en Haskell mucho . Los tipos aseguran que sus cambios a gran escala serán seguros, si los utiliza de manera inteligente. Esto ayudará a su escala de código base. Asegúrese de que sus refactorizaciones causen errores de tipo hasta que se completen.
Usa la FFI sabiamente
- El FFI hace que sea más fácil jugar con código extranjero, pero ese código extranjero puede ser peligroso.
- Tenga mucho cuidado con los supuestos sobre la forma de los datos devueltos.
Programación meta
- Un poco de Template Haskell o genéricos puede eliminar repetitivo.
Embalaje y distribución
- Utilice Cabal. No ruedes tu propio sistema de construcción. (EDITAR: en realidad es probable que desee usar Stack ahora para comenzar).
- Utilice Haddock para buenos documentos API
- Herramientas como graphmod pueden mostrar sus estructuras de módulos.
- Confíe en las versiones de la plataforma Haskell de bibliotecas y herramientas, si es posible. Es una base estable. (EDITAR: de nuevo, en estos días es probable que desee usar Stack para obtener una base estable en funcionamiento).
Advertencias
- Utilice
-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 verificación similar a la pelusa, vea la gran hlint , que sugerirá mejoras.
Con todas estas herramientas puede controlar la complejidad, eliminando tantas interacciones entre componentes como sea posible. 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 apuntarlo.
En general: descomponga las unidades lógicas de su sistema en los componentes transparentes de referencia más pequeños posibles, luego implementelos en módulos. Los entornos globales o locales para conjuntos de componentes (o componentes internos) pueden asignarse a mónadas. Utilice tipos de datos algebraicos para describir estructuras de datos centrales. Comparte esas definiciones ampliamente.
He encontrado el artículo " Arquitectura de software de enseñanza utilizando Haskell " (pdf) de Alejandro Serrano útil para pensar sobre la estructura a gran escala en Haskell.
La publicación del blog de Gabriel Las arquitecturas de programas escalables podrían ser dignas de mención.
Los patrones de diseño de Haskell difieren de los patrones de diseño convencionales en una forma importante:
Arquitectura convencional : combina varios componentes de tipo A para generar una "red" o "topología" de tipo B
Arquitectura Haskell : combina varios componentes de tipo A para generar un nuevo componente del mismo tipo A, que no se puede distinguir del carácter de sus partes sustituyentes
A menudo me sorprende que una arquitectura aparentemente elegante a menudo caiga de las bibliotecas que exhiben este agradable sentido de homogeneidad, de una manera de abajo hacia arriba. En Haskell esto es especialmente evidente: los patrones que tradicionalmente se considerarían "arquitectura descendente" tienden a ser capturados en bibliotecas como Netwire , Netwire y Cloud Haskell . Es decir, espero que esta respuesta no se interprete como un intento de reemplazar cualquiera de los otros en este hilo, solo que las opciones estructurales pueden y deben idealmente abstraerse en bibliotecas por expertos en dominios. La verdadera dificultad para construir grandes sistemas, en mi opinión, es evaluar estas bibliotecas por su "bondad" arquitectónica frente a todas sus preocupaciones pragmáticas.
Como liminalisht en los comentarios, el patrón de diseño de la categoría es otra publicación de Gabriel sobre el tema, en una vena similar.
Tal vez tenga que retroceder un paso y pensar en cómo traducir la descripción del problema a un diseño en primer lugar. 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 los campos faltantes, las instancias faltantes y los transformadores monádicos faltantes en su código, porque, por ejemplo, realiza un acceso a la base de datos desde una biblioteca que necesita cierta mónada de estado dentro de un procedimiento de IO. Y voila, ahí está el programa. El compilador alimenta tus bocetos mentales y da coherencia al diseño y al desarrollo.
De esta manera, usted 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 tiene en mente es un problema ordinario concreto. Creo que la ingeniería excesiva es la cosa más peligrosa en TI. Las cosas son diferentes cuando el problema es crear una biblioteca que abstraiga un conjunto de problemas relacionados.