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

565

¿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? :)

Dan
fuente
99
Creo que la idea central cuando se escriben programas grandes en un lenguaje funcional es módulos pequeños, especializados y sin estado que se comunican a través de la transmisión de mensajes . Por supuesto, tienes que fingir un poco porque un verdadero programa necesita estado. Creo que aquí es donde F # brilla sobre Haskell.
ChaosPandion
18
@Chaos, pero solo Haskell impone la apatridia por defecto. No tiene otra opción, y tiene que trabajar duro para introducir el estado (para romper la composición) en Haskell :-)
Don Stewart
77
@ChaosPandion: No estoy en desacuerdo, en teoría. Ciertamente, en un lenguaje imperativo (o uno funcional diseñado para transmitir mensajes), eso podría ser lo que yo haría. Pero Haskell tiene otras formas de lidiar con el estado, y tal vez me permitieron conservar más de los beneficios 'puros'.
Dan
1
Escribí un poco sobre esto en "Pautas de diseño" en este documento: community.haskell.org/~ndm/downloads/…
Neil Mitchell
55
@JonHarrop no olvidemos que si bien MLOC es una buena métrica cuando se comparan proyectos en lenguajes similares, no tiene mucho sentido para la comparación entre idiomas, especialmente con lenguajes como Haskell, donde la reutilización de código y la modularidad es mucho más fácil y segura en comparación con algunos idiomas por ahí.
Tair

Respuestas:

519

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

  • Use 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 algún alcance)
    • Ese cierto código no hace IO, no toca el disco
  • Hacer cumplir la seguridad: excepciones marcadas (Quizás / Cualquiera), evite mezclar conceptos (Word, Int, Dirección)
  • 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 los límites.

El perfilador

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

Pureza

  • Reduzca la complejidad dramáticamente eliminando el estado. El código puramente funcional escala, porque es compositivo. Todo lo que necesita es el tipo para determinar cómo usar un código: no se romperá misteriosamente cuando cambie alguna otra parte del programa.
  • Use mucha programación de estilo "modelo / vista / controlador": analice los datos externos lo antes posible en estructuras de datos puramente funcionales, opere en esas estructuras, luego, una vez que todo el trabajo esté hecho, renderice / vacíe / serialice. 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 pasas 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. Siga refactorizando 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 accede al hardware, este código es una sesión de usuario único, etc.)
  • Por ejemplo, la mónada X 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

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

Concurrencia y paralelismo

  • Sumérgete paren tu programa para vencer a la competencia con un paralelismo fácil y componible.

Refactor

  • Puede refactorar Haskell mucho . Los tipos aseguran que sus cambios a gran escala serán seguros, si está utilizando los tipos con prudencia. Esto ayudará a escalar su base de código. Asegúrese de que sus refactorizaciones causen errores de tipo hasta que se completen.

Use el 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 en los supuestos sobre la forma de los datos devueltos.

Meta programación

  • Un poco de Template Haskell o genéricos pueden eliminar repeticiones.

Empaque y distribución

  • Usa Cabal. No tires tu propio sistema de construcción. (EDITAR: en realidad probablemente quieras usar Stack ahora para comenzar).
  • Use Haddock para buenos documentos de API
  • Herramientas como graphmod pueden mostrar las estructuras de sus módulos.
  • Confíe en las versiones de bibliotecas y herramientas de la plataforma Haskell, si es posible. Es una base estable. (EDITAR: una vez más, en estos días es probable que desee usar Stack para obtener una base estable en funcionamiento).

Advertencias

  • Úselo -Wallpara 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.

Don Stewart
fuente
8
Gracias Don, su respuesta es excelente: todas estas son pautas valiosas y las consultaré regularmente. Sin embargo, supongo que mi pregunta ocurre un paso antes de que uno necesite todo esto. Lo que realmente me gustaría saber son los "modismos para mapear el diseño en tipos / funciones / clases / mónadas" ... Podría intentar inventar el mío, pero esperaba que pudiera haber un conjunto de mejores prácticas destiladas en alguna parte: o si no, recomendaciones para un código bien estructurado para leer de un sistema de gran tamaño (en lugar de, por ejemplo, una biblioteca enfocada). Edité mi publicación para hacer esta misma pregunta más directamente.
Dan
66
He agregado texto sobre la descomposición del diseño a los módulos. Su objetivo es identificar funciones lógicamente relacionadas en módulos que tengan interfaces transparentes referenciales con otras partes del sistema, y ​​utilizar tipos de datos puramente funcionales lo antes posible, tanto como sea posible, para modelar el mundo exterior de manera segura. El documento de diseño de xmonad cubre mucho de esto: xmonad.wordpress.com/2009/09/09/…
Don Stewart
3
Traté de descargar las diapositivas de los Grandes Proyectos de Ingeniería en la charla de Haskell , pero el enlace parecía estar roto. Aquí hay uno que funciona: galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj
3
Logré
Riccardo T.
3
@Heather Aunque el enlace de descarga en la página que mencioné en el comentario anterior no funciona, parece que las diapositivas todavía se pueden ver en scribd: scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
Riccardo T.
118

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.

  1. 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.

  2. 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 liftincorporar acciones monádicas en su mónada personalizada. George Wilson muestra cómo usar mtlpara 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.

usuario349653
fuente
55
¡Dos buenos puntos! Esta respuesta tiene el mérito de ser razonablemente concreta, algo que los otros no son. Sería interesante leer más debates sobre las diferentes formas de dividir un gran programa en mónadas en una pila. ¡Por favor publique enlaces a dichos artículos si tiene alguno!
Lii
66
@Lii Ben Kolera da una gran introducción práctica a este tema, y Brian Hurt analiza soluciones al problema de liftincorporar acciones monádicas en su mónada personalizada. George Wilson muestra cómo usar mtlpara 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.
liminalisht
Estoy de acuerdo en que las pilas de transformadores de mónada tienden a ser fundamentos arquitectónicos clave, pero me esfuerzo mucho para mantener a IO fuera de ellos. No siempre es posible, pero si piensa en lo que significa "y luego" en su mónada, puede descubrir que realmente tiene una continuación o un autómata en algún lugar en la parte inferior que luego puede interpretarse en IO mediante una función de "ejecución".
Paul Johnson
Como @PaulJohnson ya ha señalado, este enfoque de Monad
McBear Holden el
43

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.

augustss
fuente
14
De hecho, descubrí que la refactorización es bastante frustrante si los tipos de datos deben cambiar. Requiere modificar tediosamente la aridad de muchos constructores y coincidencias de patrones. (Estoy de acuerdo en que refactorizar funciones puras en otras funciones puras del mismo tipo es fácil, siempre que no se toquen los tipos de datos)
Dan
2
@Dan Puede escapar completamente gratis con cambios más pequeños (como simplemente agregar un campo) cuando usa registros. Algunos pueden querer que los registros sean un hábito (yo soy uno de ellos ^^ ").
MasterMastic
55
@¿Quiero decir que si cambias el tipo de datos de una función en cualquier idioma, no tienes que hacer lo mismo? No veo cómo un lenguaje como Java o C ++ te ayudaría a este respecto. Si dice que puede usar algún tipo de interfaz común que ambos tipos obedecen, entonces debería haberlo hecho con Typeclasses en Haskell.
punto
44
@semicon la diferencia para lenguajes como Java es la existencia de herramientas maduras, bien probadas y totalmente automatizadas para la refactorización. En general, estas herramientas tienen una fantástica integración de editor y eliminan una gran cantidad del tedioso trabajo asociado con la refactorización. Haskell nos proporciona un sistema de tipo brillante con el que detectar cosas que deben cambiarse en una refactorización, pero las herramientas para llevar a cabo esa refactorización son (en este momento) muy limitadas, especialmente en comparación con lo que ya estaba disponible en Java ecosistema por más de 10 años.
jsk
16

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

El arte de la programación funcional

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

comonad
fuente
11
Por muy bueno que sea Craft of FP, aprendí Haskell de él, es un texto introductorio para programadores principiantes , no para el diseño de sistemas grandes en Haskell.
Don Stewart
3
Bueno, es el mejor libro que conozco sobre el diseño de API y la ocultación de detalles de implementación. Con este libro, me convertí en un mejor programador en C ++, solo porque aprendí mejores formas de organizar mi código. Bueno, su experiencia (y respuesta) seguramente es mejor que este libro, pero Dan probablemente todavía sea un principiante en Haskell. ( where beginner=do write $ tutorials `about` Monads)
comonad
11

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:

  • Enfoques para modelar arquitectura usando diagramas;
  • Análisis de requerimientos;
  • Modelado de dominio DSL incorporado;
  • Diseño e implementación de DSL externo;
  • Mónadas como subsistemas con efectos;
  • Mónadas libres como interfaces funcionales;
  • EDSL con flechas;
  • Inversión de control utilizando eDSL monádicos libres;
  • Memoria transaccional de software;
  • Lentes;
  • Estado, Lector, Escritor, RWS, Mónadas ST;
  • Estado impuro: IORef, MVar, STM;
  • Multithreading y modelado de dominio concurrente;
  • GUI
  • Aplicabilidad de 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 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

graninas
fuente
Alexander, ¿podrías actualizar esta nota cuando tu libro esté completo para que podamos seguirlo? Salud.
Max
44
¡Por supuesto! Por ahora terminé la mitad del texto, pero es un 1/3 del trabajo general. Por lo tanto, mantenga su interés, ¡esto me inspira mucho!
graninas
2
¡Hola! Compartí mi libro en línea (solo los primeros 5 capítulos). Ver publicación en Reddit: reddit.com/r/haskell/comments/6ck72h/…
graninas
gracias por compartir y trabajar!
Max
¡Estoy verdaderamente deseando que llegue esto!
patriques
7

La publicación del blog de Gabriel Las arquitecturas de programas escalables pueden merecer una mención.

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

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

  • Arquitectura de Haskell : combine varios componentes del tipo A para generar un nuevo componente del mismo tipo A, de carácter indistinguible de sus partes sustituyentes

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.

Rehno Lindeque
fuente
3
Mencionaría otra publicación de Gabriel González sobre el patrón de diseño de la categoría . Su argumento básico es que lo que los programadores funcionales consideramos como "buena arquitectura" es realmente "arquitectura compositiva", es diseñar programas utilizando elementos que están garantizados para componer. Dado que las leyes categoría garantizan que la identidad y la asociatividad se conservan en virtud de la composición, de una arquitectura de composición se logra mediante el uso de abstracciones para los que tenemos una categoría - por ejemplo, funciones puras, acciones monádicos, tuberías, etc.
liminalisht
3

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.

revs agocorona
fuente