web-dev-qa-db-fra.com

Conception à grande échelle à Haskell?

Quel est un bon moyen de concevoir/structurer de grands programmes fonctionnels, en particulier dans Haskell?

J'ai suivi plusieurs tutoriels (écris moi-même un programme préféré, suivi de Real World Haskell en deuxième position), mais la plupart des programmes sont relativement petits et à but unique. De plus, je ne considère pas que certaines d’entre elles soient particulièrement élégantes (par exemple, les vastes tables de consultation de WYAS).

Je souhaite maintenant écrire des programmes plus volumineux, comportant davantage de pièces mobiles: acquisition de données de différentes sources, nettoyage, traitement, traitement de différentes manières, affichage dans des interfaces utilisateur, maintien, communication via des réseaux, etc. Une meilleure structure pour que ce code soit lisible, maintenable et adaptable aux exigences changeantes?

Il existe une assez grande littérature traitant de ces questions pour les grands programmes impératifs orientés objet. Les idées telles que MVC, les modèles de conception, etc. constituent des recommandations décentes pour la réalisation d'objectifs généraux tels que la séparation des préoccupations et la possibilité de réutilisation dans un style OO. De plus, les nouveaux langages impératifs se prêtent à un refactoring du type "design as you grand", auquel, à mon avis de novice, Haskell semble moins bien adapté.

Existe-t-il une littérature équivalente pour Haskell? Comment le Zoo de structures de contrôle exotiques disponibles en programmation fonctionnelle (monades, flèches, applications, etc.) est-il le mieux utilisé à cette fin? Quelles meilleures pratiques pourriez-vous recommander?

Merci!

EDIT (ceci fait suite à la réponse de Don Stewart):

@dons mentionne: "Les monades capturent les principaux types d'architecture."

Je suppose que ma question est la suivante: comment penser les principaux concepts architecturaux dans un langage purement fonctionnel?

Prenons l'exemple de plusieurs flux de données et de plusieurs étapes de traitement. Je peux écrire des analyseurs syntaxiques modulaires pour les flux de données dans un ensemble de structures de données, et je peux implémenter chaque étape de traitement en tant que fonction pure. Les étapes de traitement requises pour une donnée dépendent de sa valeur et de celle des autres. Certaines des étapes devraient être suivies d'effets secondaires tels que des mises à jour d'interface graphique ou des requêtes de base de données.

Quelle est la "bonne" façon de lier les données et les étapes d'analyse de manière agréable? On pourrait écrire une grosse fonction qui fait la bonne chose pour les différents types de données. Ou on pourrait utiliser une monade pour garder une trace de ce qui a été traité jusqu'à présent et faire en sorte que chaque étape de traitement obtienne ce dont elle a besoin ensuite de l'état de la monade. Ou bien on pourrait écrire des programmes largement séparés et envoyer des messages (je n'aime pas beaucoup cette option).

Les diapositives qu'il a liées ont une puce Things we Need: "Idiomes pour la conception de la conception sur types/fonctions/classes/monades". Quels sont les idiomes? :)

566
Dan

Je parle un peu de cela dans Engineering Large Projects in Haskell et dans le Conception et implémentation de XMonad. Dans le grand plan, l'ingénierie concerne la gestion de la complexité. Les principaux mécanismes de structuration du code dans Haskell pour gérer la complexité sont les suivants:

Le système de types

  • Utilisez le système de types pour appliquer des abstractions, simplifiant les interactions.
  • Appliquer des invariants de clés via les types
    • (par exemple, certaines valeurs ne peuvent pas échapper à une certaine portée)
    • Ce code ne fait pas IO, ne touche pas le disque
  • Appliquez la sécurité: exceptions vérifiées (peut-être/non plus), évitez de mélanger des concepts (Word, Int, Adresse)
  • De bonnes structures de données (comme les fermetures à glissière) peuvent rendre certaines classes de tests inutiles, car elles excluent par exemple erreurs hors limites statiquement.

Le profileur

  • Fournissez des preuves objectives des profils de tas et de temps de votre programme.
  • Le profilage de tas, en particulier, est le meilleur moyen d'éviter toute utilisation inutile de mémoire.

Pureté

  • Réduisez considérablement la complexité en supprimant l'état. Le code purement fonctionnel évolue, car il est compositionnel. Tout ce dont vous avez besoin, c'est du type permettant de déterminer comment utiliser du code - il ne se cassera pas mystérieusement lorsque vous modifiez une autre partie du programme.
  • Utilisez beaucoup de programmation de type "modèle/vue/contrôleur": analysez les données externes dès que possible dans des structures de données purement fonctionnelles, opérez sur ces structures, puis une fois tout le travail terminé, restituez/effacez/sérialisez. Conserve la plupart de votre code pur

Test

  • Couverture de code QuickCheck + Haskell, pour vous assurer de tester les types de types que vous ne pouvez pas vérifier.
  • GHC + RTS est idéal pour voir si vous passez trop de temps à faire du GC.
  • QuickCheck peut également vous aider à identifier des API propres et orthogonales pour vos modules. Si les propriétés de votre code sont difficiles à énoncer, elles sont probablement trop complexes. Continuez à refactoriser jusqu'à ce que vous ayez un ensemble propre de propriétés permettant de tester votre code, qui composent bien. Ensuite, le code est probablement bien conçu aussi.

Monades pour structurer

  • Les monades capturent les types d'architecture clés (ce code accède au matériel, ce code est une session mono-utilisateur, etc.)
  • Par exemple. la monade X dans xmonad, capture précisément la conception de quel état est visible pour quels composants du système.

Classes de types et types existentiels

  • Utilisez les classes de type pour fournir l'abstraction: masquer les implémentations derrière les interfaces polymorphes.

Concurrence et parallélisme

  • Faufilez par dans votre programme pour vaincre la concurrence avec un parallélisme facile et composable.

Refactor

  • Vous pouvez refactoriser beaucoup dans Haskell . Les types garantissent que vos modifications à grande échelle seront sécurisées, si vous utilisez les types à bon escient. Cela aidera votre base de code à l’échelle. Assurez-vous que vos refactorings provoqueront des erreurs de type jusqu'à la fin.

Utilisez le FFI à bon escient

  • La FFI facilite l'utilisation de codes étrangers, mais ces codes peuvent être dangereux.
  • Soyez très prudent dans les hypothèses sur la forme des données retournées.

Méta-programmation

  • Un peu de Template Haskell ou de génériques peut enlever le passe-partout.

Emballage et distribution

  • Utilisez Cabal. Ne lancez pas votre propre système de construction. (EDIT: En fait, vous voudrez probablement utiliser Stack pour commencer.).
  • Utilisez Haddock pour de bons documents API
  • Des outils tels que graphmod peuvent afficher les structures de vos modules.
  • Faites appel aux versions des bibliothèques et des outils de la plate-forme Haskell, dans la mesure du possible. C'est une base stable. (EDIT: Encore une fois, il est probable que vous souhaitiez utiliser Stack pour obtenir une base stable et opérationnelle.)

Avertissements

  • Utilisez -Wall pour garder votre code propre et sans odeur. Vous pouvez également consulter Agda, Isabelle ou Catch pour plus d'assurance. Pour des vérifications ressemblant à des peluches, voir le grand hlint , qui suggérera des améliorations.

Avec tous ces outils, vous pouvez maîtriser la complexité en éliminant autant d'interactions que possible entre les composants. Idéalement, vous avez une très grande base de code pur, ce qui est vraiment facile à maintenir, car il est compositionnel. Ce n'est pas toujours possible, mais cela vaut la peine d'être visé.

En général: décomposez les unités logiques de votre système en composants les plus petits possible référentiellement transparents, puis implémentez-les dans des modules. Les environnements globaux ou locaux pour des ensembles de composants (ou à l'intérieur de composants) peuvent être mappés à des monades. Utilisez des types de données algébriques pour décrire les structures de données principales. Partagez ces définitions largement.

519
Don Stewart

Don vous a donné la plupart des détails ci-dessus, mais voici mes deux cents pour avoir fait des programmes très détaillés tels que les démons système de Haskell.

  1. En fin de compte, vous vivez dans une pile de transformateurs monades. Au bas est IO. Au-dessus de cela, chaque module principal (au sens abstrait, pas dans le sens du module dans un fichier) mappe son état nécessaire dans une couche de cette pile. Donc, si votre code de connexion à la base de données est caché dans un module, vous écrivez le tout pour qu'il soit sur un type MonadReader Connection m => ... -> m ... modules devant être conscients de son existence. Vous pourriez vous retrouver avec une couche portant votre connexion à la base de données, une autre votre configuration, une troisième vos différents sémaphores et mvars pour la résolution du parallélisme et de la synchronisation, une autre vos descripteurs de fichier journal, etc.

  2. Déterminez votre traitement des erreurs en premier . La plus grande faiblesse actuelle de Haskell dans les grands systèmes réside dans la pléthore de méthodes de traitement des erreurs, y compris les méthodes moche telles que Maybe (ce qui est faux car vous ne pouvez pas renvoyer d’informations sur ce qui a mal tourné; utilisez toujours Either à la place de Maybe à moins que vous ne le vouliez vraiment. juste signifier des valeurs manquantes). Déterminez comment vous allez le faire en premier et configurez des adaptateurs à partir des divers mécanismes de traitement des erreurs que vos bibliothèques et autres codes utilisent dans votre dernier. Cela vous sauvera un monde de chagrin plus tard.

Addendum (extrait des commentaires; grâce à Lii & liminalisht ) -
autres discussions sur les différentes manières de diviser un programme volumineux en monades en pile:

Ben Kolera donne une excellente introduction pratique à ce sujet et Brian Hurt discute des solutions au problème des actions monadiques lift dans votre monade personnalisée. George Wilson montre comment utiliser mtl pour écrire du code qui fonctionne avec n’importe quelle monade qui implémente les classes de types requises, plutôt que votre type de monade personnalisé. Carlo Hamalainen a écrit de brèves notes utiles résumant le discours de George.

118
user349653

Concevoir de gros programmes en Haskell n’est pas si différent de le faire dans d’autres langues. Programmer en grand consiste à décomposer votre problème en éléments gérables et à les intégrer. la langue de mise en œuvre est moins importante.

Cela dit, dans un grand modèle, il est agréable d’essayer de tirer parti du système de typographie pour s’assurer que vous ne pouvez assembler vos pièces que de manière correcte. Cela peut impliquer des types newtype ou fantôme pour que les éléments qui semblent avoir le même type soient différents.

Pour ce qui est de refactoriser le code au fur et à mesure, la pureté est une aubaine, alors essayez de garder le plus de code possible pur. Le code pur est facile à refactoriser, car il n’a pas d’interaction cachée avec d’autres parties de votre programme.

43
augustss

J'ai appris la programmation fonctionnelle structurée pour la première fois avec ce livre . Ce n'est peut-être pas exactement ce que vous recherchez, mais pour les débutants en programmation fonctionnelle, cela peut être l'une des meilleures premières étapes pour apprendre à structurer des programmes fonctionnels - indépendamment de l'échelle. À tous les niveaux d'abstraction, la conception doit toujours avoir des structures clairement disposées.

L'art de la programmation fonctionnelle

The Craft of Functional Programming

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

16
comonad

J'écris actuellement un livre intitulé "Design fonctionnel et architecture". Il vous fournit un ensemble complet de techniques permettant de créer une grande application en utilisant une approche purement fonctionnelle. Il décrit de nombreux modèles fonctionnels et idées lors de la création d'une application "Andromède" de type SCADA permettant de contrôler à partir de vaisseaux spatiaux. Ma langue principale est le Haskell. Le livre couvre:

  • Approches de la modélisation d'architecture à l'aide de diagrammes;
  • Analyse des besoins;
  • Modélisation de domaine DSL intégré;
  • Conception et mise en œuvre de DSL externes;
  • Les monades en tant que sous-systèmes avec effets;
  • Monades libres comme interfaces fonctionnelles;
  • EDSL Arrowisés;
  • Inversion de contrôle en utilisant des eDSL monadiques libres;
  • Logiciel Transactional Memory;
  • Lentilles;
  • État, lecteur, écrivain, RWS, monades ST;
  • Etat impur: IORef, MVar, STM;
  • Modélisation multithreading et simultanée;
  • Interface graphique;
  • Applicabilité des techniques et approches classiques telles que UML, SOLID, GRASP;
  • Interaction avec des sous-systèmes impurs.

Vous pouvez vous familiariser avec le code du livre ici , et le code de projet 'Andromeda' .

J'espère terminer ce livre à la fin de 2017. En attendant, vous pouvez lire mon article "Design et architecture dans la programmation fonctionnelle" (Rus) ici .

UPDATE

J'ai partagé mon livre en ligne (5 premiers chapitres). Voir post sur Reddit

11
graninas

Le billet de blog de Gabriel architectures de programme évolutives mérite peut-être une mention.

Les modèles de conception Haskell se distinguent des modèles de conception traditionnels d'une manière importante:

  • Architecture conventionnelle : combinez plusieurs composants de type A pour générer un "réseau" ou une "topologie" de type B

  • Architecture Haskell : combinez plusieurs composants de type A pour générer un nouveau composant du même type A, dont le caractère ne se distingue pas de ses parties substituantes.

Il me semble souvent qu'une architecture d'apparence élégante a souvent tendance à se détacher des bibliothèques qui présentent ce sens de l'homogénéité de Nice, de manière ascendante. En Haskell, cela est particulièrement apparent: les motifs traditionnellement considérés comme "architecture descendante" ont tendance à être capturés dans des bibliothèques telles que mvc , Netwire et Cloud Haskell . C’est-à-dire que j’espère que cette réponse ne sera pas interprétée comme une tentative de remplacer les autres de ce fil, mais que les choix structurels peuvent et devraient idéalement être résumés dans les bibliothèques par des experts du domaine. La vraie difficulté à construire de grands systèmes, à mon avis, consiste à évaluer ces bibliothèques sur leur "qualité" architecturale par rapport à toutes vos préoccupations pragmatiques.

Comme liminalisht mentionne dans les commentaires, Le modèle de conception de la catégorie est un autre billet de Gabriel sur le sujet, dans le même esprit.

7
Rehno Lindeque

J'ai trouvé le papier "( Enseigner l'architecture logicielle à l'aide de Haskell " (pdf) par Alejandro Serrano utile pour réfléchir à la grande échelle structure en Haskell.

5
haroldcarr

Peut-être devez-vous faire un pas en arrière et réfléchir à la manière de traduire la description du problème en une conception. Puisque Haskell a un si haut niveau, il peut capturer la description du problème sous la forme de structures de données, les actions en tant que procédures et la transformation pure en tant que fonctions. Ensuite, vous avez un design. Le développement commence lorsque vous compilez ce code et recherchez des erreurs concrètes concernant des champs manquants, des instances manquantes et des transformateurs monadiques manquants dans votre code, car vous effectuez par exemple une base de données Access à partir d'une bibliothèque nécessitant un certain état monad dans un IO procédure. Et voila, il y a le programme. Le compilateur nourrit vos esquisses mentales et donne une cohérence à la conception et au développement.

De cette manière, vous bénéficiez de l'aide de Haskell depuis le début et le codage est naturel. Je ne voudrais pas faire quelque chose de "fonctionnel" ou de "pur" ou assez général si ce que vous avez en tête est un problème ordinaire concret. Je pense que la sur-ingénierie est la chose la plus dangereuse en informatique. Les choses sont différentes lorsque le problème consiste à créer une bibliothèque qui résume un ensemble de problèmes connexes.

3
agocorona