2010-06-20 18 views
534

¿Cuál es una buena manera de diseñar/estructurar programas funcionales grandes, especialmente en Haskell?Diseño a gran escala en Haskell?

He pasado por muchos de los 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, conservarlo, comunicarme a través de redes, ¿Cómo podría uno estructurar mejor dicho código para ser legible, mantenible y adaptable a los requisitos cambiantes?

Hay una gran cantidad de literatura 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.

¿Existe 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 un seguimiento de la respuesta de Don Stewart):

@dons mencionan: "mónadas capturar diseños arquitectónicos clave en los tipos"

Supongo que mi pregunta es: ¿cómo se debe 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 viñeta: "Modismos para el diseño de mapeo en tipos/funciones/clases/mónadas". ¿Cuáles son los modismos? :)

+9

Creo que la idea principal cuando se escriben programas grandes en un lenguaje funcional es * módulos pequeños, especializados y sin estado que se comunican a través del envío 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 de forma predeterminada. No tiene otra opción, y tiene que trabajar duro para introducir el estado (para romper la composicionalidad) en Haskell :-) –

+0

@Don - Sí, lo sé, pero soy uno de los mejores ambos mundos tipo chicos. – ChaosPandion

Respuesta

494

Hablo un poco acerca de esto en Engineering Large Projects in Haskell y en el Design and Implementation of XMonad. Ingeniería en general se trata de la gestión de la complejidad. Los principales mecanismos de estructuración de código en Haskell para la gestión de la complejidad son:

El sistema de tipos

  • uso del sistema de tipos para hacer cumplir las abstracciones, lo que simplifica la interacción.
  • cumplir invariantes clave a través de tipos
    • (por ejemplo, que ciertos valores no pueden escapar de un cierto margen)
    • que cierto código no hace IO, no toca el disco
  • Hacer cumplir la seguridad: Las excepciones comprobadas (Tal vez/O bien), evite 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 perfilador

  • proporcionan evidencia objetiva de perfiles montón de tiempo y 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

  • Reducir la complejidad drásticamente mediante la eliminación de 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 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é terminado, renderice/descargue/serialice. Mantiene la mayor parte de su código puro

Prueba

  • QuickCheck + cobertura de código Haskell, para asegurarse de que está probando las cosas que no se puede comprobar con los tipos.
  • GHC + RTS es ideal para ver si está 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 la estructuración de

  • Mónadas capturar diseños arquitectónicos claves de tipos (el código tiene acceso a hardware, este código es una sesión de 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.

tipo clases y tipos existenciales

  • clases de uso de tipo para proporcionar la abstracción: ocultar detrás implementaciones de interfaces polimórficos.

concurrencia y paralelismo

  • par furtivo en su programa para vencer a la competencia con fácil paralelismo, componibles.

Refactor

  • Puede refactorar 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.

Uso del FFI sabiamente

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

programación Meta

  • Un poco de Plantilla Haskell o genéricos puede eliminar repetitivo.

embalaje y distribución

  • Uso Cabal. No mueva su propio sistema de compilación. (EDITAR: En realidad, es probable que desee utilizar Stack ahora para comenzar).
  • Utilice Haddock para buenos documentos API
  • Las herramientas como graphmod pueden mostrar las estructuras de sus módulos.
  • Confíe en las versiones de la plataforma Haskell de bibliotecas y herramientas, si es posible. Es una base estable. (EDIT: Una vez más, estos días es probable que quieren utilizar Stack para conseguir una base estable y funcionando.)

Advertencias

  • Uso -Wall para mantener su código limpio de olores. También puede mirar a Agda, Isabelle o Catch para obtener más seguridad. Para verificaciones parecidas a pelusas, vea el 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 de composición. Eso no siempre es posible, pero vale la pena apuntar.

En general: descomponga 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.

+7

Gracias Don, tu respuesta es excelente: estas son todas pautas valiosas y me referiré a ellas regularmente. Creo que mi pregunta ocurre un paso antes de que uno necesite todo esto, sin embargo. Lo que realmente me gustaría saber son los "Modismos para el diseño de mapeo sobre tipos/funciones/clases/mónadas" ... Podría tratar de inventar el mío, pero esperaba que pudiera haber un conjunto de mejores prácticas destiladas en algún lugar: o si no, recomendaciones para leer un código bien estructurado de un sistema de gran tamaño (en lugar de, digamos, una biblioteca enfocada). Edité mi publicación para hacer esta misma pregunta más directamente. – Dan

+5

He agregado algo de texto sobre la descomposición del diseño a los módulos. Su objetivo es identificar las funciones lógicamente relacionadas en módulos que tienen interfaces referencialmente transparentes con otras partes del sistema, y ​​utilizar tipos de datos puramente funcionales lo más pronto posible, lo más posible, para modelar el mundo exterior de forma segura. El documento de diseño de xmonad cubre mucho de esto: http://xmonad.wordpress.com/2009/09/09/the-design-and-implementation-of-xmonad/ –

+0

¡Gracias de nuevo! El documento de diseño de xmonad es justo lo que estaba buscando. Es hora de leer un código ... – Dan

43

Diseñar grandes programas 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 unirlas; 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 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 la mayor cantidad de 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.

+13

De hecho, encontré que la refactorización es bastante frustrante, si los tipos de datos necesitan cambiar. Se requiere modificar tediosamente la cantidad de constructores y coincidencias de patrones . (Estoy de acuerdo en que la refactorización de funciones puras en otras funciones puras del mismo tipo es fácil, siempre y cuando no se toquen los tipos de datos) – Dan

+2

@Dan Puede liberarse completamente 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

+2

@Dan quiero decir si cambia el tipo de datos de una función en cualquier idioma, no tiene que hacer el ¿mismo? no veo cómo un lenguaje como Java o C++ te ayudaría a este respecto. Si dices que puedes usar algún tipo de interfaz común que ambos tipos obedezcan, entonces deberías haber estado haciendo eso con Typeclasses en Haskell. – semicolon

116

Don le dio la mayor parte de los detalles anteriores, pero aquí está mi granito de arena de hacer programas realmente nítidos como los demonios del sistema en Haskell.

  1. Al final, usted vive en una pila de transformadores de mónada. 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. Usted podría terminar con una capa llevar su conexión a la base de datos, la configuración de otro, un tercer sus diversos semáforos y MVARs para la resolución de paralelismo y sincronización, otro de sus manijas del archivo de registro, etc.

  2. figura a cabo el manejo de su error 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.

Adición (extraído de los comentarios, gracias a Lii & liminalisht) —
discusión más acerca de las diferentes maneras de cortar un programa de gran tamaño en las mónadas en una pila:

Ben Kolera da una gran introducción práctica a este tema, y ​​Brian Hurt discute soluciones al problema de lift ing 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.

+5

¡Dos puntos buenos! Esta respuesta tiene el mérito de ser razonablemente concreta, algo que los otros no son. Sería interesante leer más discusión sobre diferentes maneras de dividir un gran programa en mónadas en una pila. ¡Publique enlaces a tales artículos si tiene alguno! – Lii

+4

@Lii [Ben Kolera] (https://www.youtube.com/watch?v=pzouxmWiemg) ofrece una gran introducción práctica a este tema, y ​​[Brian Hurt] (https://www.youtube.com/watch ? v = 8t8fjkISjus) discute soluciones al problema de 'levantar acciones monádicas en su mónada personalizada. [George Wilson] (https://www.youtube.com/watch?v=GZPup5Iuaqw) 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] (http://carlo-hamalainen.net/blog/2015/7/20/classy-mtl) ha escrito algunas notas breves y útiles que resumen la charla de George. – liminalisht

15

Aprendí estructurado programación funcional la primera vez con this book. 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 oficio de programación funcional

The Craft of Functional Programming

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

+11

Tan grande como el Craft of FP es, aprendí Haskell de él, es un * texto introductorio * para * programadores principiantes *, no para el diseño de sistemas grandes en Haskell. –

+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++, simplemente porque aprendí mejores formas de organizar mi código. Bueno, tu experiencia (y respuesta) es seguramente mejor que este libro, pero es posible que Dan aún sea un "principiante" en Haskell. ('donde beginner = do write $ tutorials \' about \ 'Monads') – comonad

2

Tal vez usted tiene que ir un paso atrás y pensar en cómo traducir la descripción del problema a un diseño en el 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.

7

La publicación de Gabriel en el blog Scalable program architectures podría valer una mención.

patrones

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

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

  • Haskell architecture: Combina varios componentes de tipo A a generar un nuevo componente del mismo tipo A, indistinguibles en carácter de sus partes sustituyentes

A menudo me ocurre que una arquitectura aparentemente elegante a menudo tiende a caerse de las bibliotecas que presentan este agradable sensación de homogeneidad, de una manera ascendente. En Haskell esto es especialmente evidente: los patrones que tradicionalmente se considerarían como "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, The category design pattern es otra publicación de Gabriel sobre el tema, en un sentido similar.

+3

Mencionaré otra publicación de Gabriel Gonzalez en el [category design pattern] (http://www.haskellforall.com/2012/08/the- category-design-pattern.html). Su argumento básico es que lo que los programadores funcionales consideramos como "buena arquitectura" es en realidad "arquitectura de la composición": es el diseño de programas que utilizan elementos que se garantiza que componen. Dado que las leyes de categoría garantizan que la identidad y la asociatividad se conservan en la composición, se logra una arquitectura de composición mediante el uso de abstracciones para las que tenemos una categoría, p. funciones puras, acciones monádicas, tuberías, etc. – liminalisht

11

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:

  • Acercamientos al modelado de arquitectura usando diagramas;
  • Análisis de requisitos;
  • Modelado de dominio DSL integrado;
  • 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 usando eDSL monádicos gratuitos;
  • Software Transactional Memory;
  • Lentes;
  • Estado, lector, escritor, RWS, ST mónadas;
  • Estado impuro: IORef, MVar, STM;
  • Multithreading y modelado de dominio concurrente;
  • GUI;
  • Aplicabilidad de técnicas y enfoques convencionales tales 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, puedes leer mi artículo "Diseño y Arquitectura en Programación Funcional" (Rus) here.

ACTUALIZACIÓN

compartí mi libro en línea (los primeros 5 capítulos). Ver post on Reddit

+0

Alexander, podría actualizar amablemente esta nota cuando su libro esté completo, para que podamos seguirlo. Aclamaciones. – Max

+4

¡Claro! Por ahora, terminé la mitad del texto, pero es un 1/3 del trabajo total. Entonces, mantén tu interés, ¡esto me inspira mucho! – GAS

+2

¡Hola! Compartí mi libro en línea (solo los primeros 5 capítulos). Ver la publicación en Reddit: https://www.reddit.com/r/haskell/comments/6ck72h/functional_design_and_architecture/ – GAS

Cuestiones relacionadas