Pregunta Diseño a gran escala en Haskell? [cerrado]


¿Cuál es una buena forma de diseñar / estructurar grandes programas funcionales, especialmente en Haskell?

He pasado por muchos tutoriales (Write Yourself a Scheme es mi favorito, con Real World Haskell en un segundo plano), 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, limpiarlo, procesarlo de varias maneras, mostrarlo en las interfaces de usuario, mantenerlo, comunicarme a través de redes, etc. Una de las mejores estructuras es el código para ser legible, mantenible y adaptable a los requisitos cambiantes.

Existe una literatura bastante extensa que aborda estas cuestiones para grandes programas imperativos orientados a objetos. Ideas como MVC, patrones de diseño, etc. son recetas decentes para realizar 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', al cual, en mi opinión de novato, Haskell parece menos adecuado.

¿Hay alguna literatura equivalente para Haskell? ¿Cómo está disponible el zoológico de estructuras de control exóticas en la programación funcional (mónadas, flechas, aplicativo, etc.) mejor empleadas para este propósito? ¿Qué mejores prácticas podrías recomendar?

¡Gracias!

EDITAR (esto es una continuación 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 sobre 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 las secuencias 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 de los demás. Algunos de los pasos deberían ir seguidos de efectos colaterales, como actualizaciones de la GUI o consultas a la base de datos.

¿Cuál es la forma "correcta" de vincular los datos y los pasos de análisis de una manera agradable? Uno podría escribir una gran función que hace lo correcto para los diversos 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 de procesamiento obtenga lo que necesite a partir del estado de la mónada. O uno podría escribir programas bastante separados y enviar mensajes (no me gusta mucho esta opción).

Las diapositivas que vinculan tienen una cosa que necesitamos Bullet: "Modismos para el diseño de mapeo en tipos / funciones / clases / mónadas ". ¿Cuáles son los modismos? :)


552
2018-06-20 01:21


origen


Respuestas:


Hablo un poco acerca de 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 tipo

  • Use el sistema de tipos para imponer las abstracciones, simplificando las interacciones.
  • Aplicar invariantes de claves por tipos
    • (por ejemplo, que ciertos valores no pueden escapar de algún ámbito)
    • Ese cierto código no hace IO, no toca el disco
  • Hacer cumplir la seguridad: excepciones marcadas (Tal vez / O bien), evitar mezclar conceptos (Word, Int, Dirección)
  • Las buenas estructuras de datos (como las cremalleras) pueden hacer innecesarias algunas clases de pruebas, ya que excluyen, por ejemplo, errores fuera de límites estáticamente.

El generador de perfiles

  • Proporcione evidencia objetiva de los perfiles de montón y tiempo de su programa.
  • La creación de perfiles de montón, en particular, es la mejor manera de garantizar que no se use innecesariamente la memoria.

Pureza

  • Reduzca la complejidad dramáticamente al eliminar el estado. Escalas de código puramente funcionales, porque es composicional. 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 de estilo "modelo / vista / controlador": analice datos externos lo más pronto posible en estructuras de datos puramente funcionales, opere en esas estructuras, luego, una vez que todo el trabajo esté terminado, renderice / descargue / serialice. Mantiene la mayor parte de tu código puro

Pruebas

  • Cobertura del código QuickCheck + Haskell, para asegurarse de que está probando las cosas que no puede verificar con los tipos.
  • GHC + RTS es ideal para ver si estás pasando 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. Mantenga la refactorización hasta que tenga un conjunto limpio de propiedades que puedan probar su código, que compongan bien. Entonces el código probablemente también esté bien diseñado.

Mónadas para estructurar

  • Las mónadas capturan diseños arquitectónicos clave en tipos (este código tiene acceso al hardware, este código es una sesión para un solo usuario, etc.)
  • P.ej. la mónada X en xmonad, captura precisamente el diseño para qué estado es visible para qué componentes del sistema.

Clases de tipos y tipos existenciales

  • Utilice clases de tipos para proporcionar abstracción: oculte las implementaciones detrás de las interfaces polimórficas.

Concurrencia y paralelismo

  • Furtivo par en su programa para vencer a la competencia con un paralelismo fácil y composable.

Refactor

  • Puede refactorizar en Haskell mucho. Los tipos aseguran que sus cambios a gran escala serán seguros, si usa tipos sabiamente. Esto ayudará a la escala de su base de código. Asegúrese de que las refactorizaciones causen errores de tipo hasta que se completen.

Use el FFI sabiamente 

  • El FFI hace que jugar con código extranjero sea más fácil, pero ese código extraño puede ser peligroso.
  • Tenga mucho cuidado en las suposiciones sobre la forma de los datos devueltos.

Meta programación

  • Un poco de Template Haskell o genéricos pueden eliminar un texto repetitivo.

Envasado y distribución

  • Usa Cabal. No mueva su propio sistema de compilación. (EDITAR: En realidad, es probable que desee utilizar Apilar ahora para comenzar.).
  • Usa Haddock para obtener buenos documentos API
  • Herramientas como graphmod puede mostrar las estructuras de tu módulo
  • Confíe en las versiones de bibliotecas y herramientas de la Plataforma Haskell, si es posible. Es una base estable. (EDITAR: Nuevamente, en estos días es probable que desee utilizar Apilar para obtener una base estable y funcionando).

Advertencias

  • Utilizar -Wall para mantener tu código limpio de olores. También puede mirar a Agda, Isabelle o Catch para obtener más seguridad. Para una comprobación parecida a una pelusa, vea la gran Hlint, lo 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 de composición. Eso no siempre es posible, pero vale la pena apuntar.

En general: descomponer las unidades lógicas de su sistema en los componentes referencialmente transparentes 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. Use tipos de datos algebraicos para describir las estructuras de datos centrales. Comparte esas definiciones ampliamente.


510
2018-06-20 01:42



Don te dio la mayoría de los detalles de arriba, pero aquí está mi granito de arena de hacer programas realmente esenciales como los demonios del sistema en Haskell.

  1. Al final, vives en una pila de transformadores de mónadas. En la parte inferior es IO. Por encima de eso, cada módulo principal (en el sentido abstracto, no el sentido de módulo en un archivo) mapea su estado necesario en una capa en esa pila. Entonces, si tienes el código de conexión de tu base de datos oculto en un módulo, lo escribes para que sea sobre un tipo de conexión MonadReader m => ... -> m ... y tus funciones de base de datos siempre pueden obtener su conexión sin funciones de otros módulos que deben conocer su existencia. Puede terminar con una capa con su conexión a la base de datos, otra con su configuración, un tercero con sus diversos semáforos y mvares para la resolución de paralelismo y sincronización, otra que maneje su archivo de registro, etc.

  2. Descubre tu manejo de errores primero. La mayor debilidad en este momento para Haskell en sistemas más grandes es la plétora de métodos de manejo de errores, incluidos los pésimos como Maybe (que es incorrecto porque no se puede devolver ninguna información sobre lo que salió mal; siempre use O bien en vez de Maybe a menos que realmente sólo quiero decir valores faltantes). 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.

Apéndice (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 ofrece una gran introducción práctica a este tema, y Brian Hurt discute soluciones al problema de liftlas acciones monádicas en 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 notas breves y útiles que resumen la charla de George.


117
2018-06-21 10:39



Diseñar programas grandes en Haskell no es tan diferente de hacerlo en otros idiomas. La programación en grande se trata de dividir su problema en piezas manejables y cómo unirlas; el lenguaje de implementación es menos importante.

Dicho esto, en un diseño grande, es bueno tratar de aprovechar el sistema de tipos para asegurarse de que solo puede ajustar sus piezas de una manera correcta. Esto podría implicar tipos newtype o phantom para hacer que las cosas que parecen ser del mismo tipo sean diferentes.

Cuando se trata de refactorizar el código a medida que avanza, la pureza es una gran ventaja, 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 tu programa.


43
2018-06-20 09:29



Aprendí estructurado programación funcional 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

The Craft of Functional Programming

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/


16
2017-10-17 23:23



Actualmente estoy escribiendo un libro con el título "Diseño funcional y arquitectura". Le proporciona un conjunto completo de técnicas sobre cómo 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 naves espaciales desde cero. Mi idioma principal es Haskell. El libro cubre:

  • Aproximaciones al modelado de arquitectura usando diagramas;
  • Análisis de requerimientos;
  • Modelado de dominio DSL integrado;
  • Diseño e implementación de DSL externo;
  • Las mónadas como subsistemas con efectos;
  • Mónadas libres como interfaces funcionales;
  • EDSL con flechas;
  • Inversión del control usando eDSL monádicos gratuitos;
  • Memoria transaccional de software;
  • Lentes;
  • State, Reader, Writer, RWS, ST mónadas;
  • Estado impuro: IORef, MVar, STM;
  • Multithreading y modelado de dominio concurrente;
  • GUI;
  • Aplicabilidad de las técnicas y enfoques convencionales como UML, SOLID, GRASP;
  • Interacción con subsistemas impuros.

Puede familiarizarse con el código del libro aquí, y el 'Andrómeda' código de proyecto.

Espero terminar este libro a fines de 2017. Hasta que eso suceda, pueden 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 publicar en Reddit


11
2017-11-20 05:25



Publicación de Gabriel en el blog Arquitecturas de programa escalables podría valer una mención.

Los patrones de diseño de Haskell difieren de los patrones de diseño convencionales en uno   forma importante:

  • Arquitectura convencional: Combine varios componentes de   escriba A para generar una "red" o "topología" de tipo B

  • Arquitectura Haskell: Combine varios componentes de tipo A para   generar un nuevo componente del mismo tipo A, indistinguible en   personaje de sus partes sustituyentes

A menudo me sorprende que una arquitectura aparentemente elegante a menudo tiende a desaparecer de las bibliotecas que muestran esta agradable sensación de homogeneidad, de una manera ascendente. En Haskell esto es especialmente evidente: los patrones que tradicionalmente se considerarían "arquitectura descendente" tienden a capturarse en bibliotecas como mvc, Netwire y Cloud 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 elecciones estructurales pueden y deben idealmente abstraerse en las bibliotecas por expertos en el dominio. La verdadera dificultad en la construcción de sistemas grandes, en mi opinión, es evaluar estas bibliotecas sobre 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.


7
2017-08-31 10:07



He encontrado el papel "Enseñanza de la arquitectura de software usando Haskell"(pdf) por Alejandro Serrano útil para pensar sobre la estructura a gran escala en Haskell.


5
2018-02-18 19:24



Tal vez deba dar un paso atrás y pensar en cómo traducir la descripción del problema a un diseño en primer lugar. Como 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 una base de datos Acceso desde una biblioteca que necesita una mónada de estado determinada dentro de un procedimiento IO. Y voila, está el programa. El compilador alimenta sus bocetos mentales y le da coherencia al diseño y al desarrollo.

De esta forma, se beneficiará de la ayuda de Haskell desde el principio, y la codificación es natural. No me gustaría hacer algo "funcional" o "puro" o bastante general si lo que tienes en mente es un problema ordinario concreto. Creo que la sobreingenierí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.


3