563 votes

Conception à grande échelle en Haskell ?

Quelle est une bonne façon de concevoir/structurer de grands programmes fonctionnels, notamment en Haskell ?

J'ai parcouru un certain nombre de tutoriels (Write Yourself a Scheme étant mon préféré, avec Real World Haskell en deuxième position) - mais la plupart des programmes sont relativement petits et à usage unique. De plus, je ne considère pas que certains d'entre eux soient particulièrement élégants (par exemple, les vastes tables de consultation dans WYAS).

J'ai maintenant envie d'écrire des programmes plus importants, avec plus de pièces mobiles - acquérir des données à partir d'une variété de sources différentes, les nettoyer, les traiter de diverses manières, les afficher dans des interfaces utilisateur, les persister, communiquer sur des réseaux, etc. Comment structurer au mieux un tel code pour qu'il soit lisible, maintenable et adaptable à l'évolution des besoins ?

Il existe une littérature assez importante traitant de ces questions pour les grands programmes impératifs orientés objet. Des idées comme MVC, les modèles de conception, etc. sont des prescriptions décentes pour réaliser des objectifs généraux comme la séparation des préoccupations et la réutilisabilité dans un style OO. De plus, les langages impératifs les plus récents se prêtent à un style de refactoring "design as you grow" auquel, selon mon opinion de novice, Haskell semble moins bien adapté.

Existe-t-il une littérature équivalente pour Haskell ? Comment utiliser au mieux le zoo de structures de contrôle exotiques disponibles dans la programmation fonctionnelle (monades, flèches, applicatifs, etc.) ? Quelles sont les meilleures pratiques que vous pourriez recommander ?

Gracias.

EDIT (ceci est un suivi de la réponse de Don Stewart) :

@dons a mentionné : "Les monades capturent des conceptions architecturales clés dans les types."

Je suppose que ma question est la suivante : comment penser aux conceptions architecturales clés dans un langage fonctionnel pur ?

Prenons l'exemple de plusieurs flux de données et de plusieurs étapes de traitement. Je peux écrire des analyseurs modulaires pour les flux de données vers un ensemble de structures de données, et je peux implémenter chaque étape de traitement comme une fonction pure. Les étapes de traitement requises pour un élément de données dépendront de sa valeur et de celle des autres. Certaines de ces étapes doivent être suivies d'effets secondaires tels que des mises à jour de l'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 syntaxique d'une manière agréable ? On pourrait écrire une grande fonction qui fait la bonne chose pour les différents types de données. Ou on pourrait utiliser une monade pour garder la 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 à partir de l'état de la monade. On peut aussi écrire des programmes largement séparés et envoyer des messages (je n'aime pas trop cette option).

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

9 votes

Je pense que l'idée principale quand on écrit de gros programmes dans un langage fonctionnel est de petits modules spécialisés et apatrides communiquant par passage de messages . Bien sûr, il faut faire un peu semblant car un vrai programme a besoin d'un état. Je pense que c'est là que F# brille par rapport à Haskell.

18 votes

Vous n'avez pas le choix, et vous devez travailler dur pour introduire un état (pour briser la compositionnalité) en Haskell :-)

0 votes

@Don - Oui, je sais, mais je suis l'un de ces types qui ont le meilleur des deux mondes.

516voto

Don Stewart Points 94361

J'en parle un peu dans Ingénierie des grands projets en Haskell et dans le Conception et mise en œuvre de XMonad. L'ingénierie dans son ensemble consiste à gérer la complexité. Les principaux mécanismes de structuration du code en Haskell pour gérer la complexité sont les suivants :

Le système de type

  • Utiliser le système de types pour renforcer les abstractions et simplifier les interactions.
  • Appliquer les invariants des clés via les types
    • (par exemple, que certaines valeurs ne peuvent pas échapper à une certaine portée)
    • Ce certain code ne fait pas d'entrée/sortie, ne touche pas le disque.
  • Garantir la sécurité : exceptions vérifiées (Maybe/Either), éviter de mélanger les concepts (Word, Int, Address).
  • De bonnes structures de données (comme les fermetures à glissière) peuvent rendre certaines catégories de tests inutiles, car elles éliminent par exemple les erreurs hors limites de manière statique.

Le profileur

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

Pureté

  • Réduire considérablement la complexité en supprimant l'état. Le code purement fonctionnel est évolutif, car il est compositionnel. Tout ce dont vous avez besoin est le type pour déterminer comment utiliser un certain code - il ne se cassera pas mystérieusement lorsque vous modifierez une autre partie du programme.
  • Utiliser beaucoup de programmation de style "modèle/vue/contrôleur" : analyser les données externes dès que possible dans des structures de données purement fonctionnelles, opérer sur ces structures, puis une fois que tout le travail est fait, rendre/flush/serialiser. Cela permet de garder la majeure partie de votre code pur

Essais

  • QuickCheck + Haskell Code Coverage, pour s'assurer que vous testez les choses que vous ne pouvez pas vérifier avec les types.
  • 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 à remanier jusqu'à ce que vous ayez un ensemble propre de propriétés qui permettent de tester votre code, qui se composent bien. Alors le code est probablement bien conçu aussi.

Monades pour la structuration

  • Les monades capturent des conceptions architecturales clés dans les types (ce code accède au matériel, ce code est une session mono-utilisateur, etc.)
  • Par exemple, le 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

  • Utiliser les classes de type pour fournir une abstraction : cacher les implémentations derrière des interfaces polymorphes.

Concurrence et parallélisme

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

Refactor

  • Vous pouvez refactorer en Haskell beaucoup . Les types garantissent que vos modifications à grande échelle seront sûres, si vous utilisez les types à bon escient. Cela aidera votre base de code à évoluer. Assurez-vous que vos refactorings causeront des erreurs de type jusqu'à ce qu'ils soient terminés.

Utiliser la FFI à bon escient

  • La FFI permet de jouer plus facilement avec le code étranger, mais ce code étranger peut être dangereux.
  • Soyez très prudent dans vos hypothèses sur la forme des données retournées.

Métaprogrammation

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

Conditionnement et distribution

  • Utilisez Cabal. Ne créez pas votre propre système de construction. (EDIT : En fait, vous voulez probablement utiliser Pile maintenant pour commencer).
  • Utiliser Haddock pour de bons documents d'API
  • Des outils comme graphmod peut montrer les structures de vos modules.
  • Utilisez, dans la mesure du possible, les versions des bibliothèques et des outils de la plate-forme Haskell. Il s'agit d'une base stable. (EDIT : Encore une fois, de nos jours, vous voudrez probablement utiliser Pile pour mettre en place une base stable).

Avertissements

  • Utilice -Wall pour que votre code soit exempt d'odeurs. Vous pouvez aussi regarder Agda, Isabelle ou Catch pour plus d'assurance. Pour une vérification de type lint, voir l'excellent hlint qui suggérera des améliorations.

Grâce à tous ces outils, vous pouvez maîtriser la complexité, en supprimant autant d'interactions que possible entre les composants. Idéalement, vous disposez d'une très grande base de code pur, qui est vraiment facile à maintenir, puisqu'il est compositionnel. Ce n'est pas toujours possible, mais cela vaut la peine d'y tendre.

En général : décomposer les unités logiques de votre système en composants les plus petits possibles, transparents sur le plan référentiel, puis les mettre en œuvre dans des modules. Les environnements globaux ou locaux pour les ensembles de composants (ou à l'intérieur des composants) peuvent être représentés par des monades. Utilisez des types de données algébriques pour décrire les structures de données principales. Partagez largement ces définitions.

8 votes

Merci Don, votre réponse est excellente - ce sont toutes des directives précieuses et je m'y référerai régulièrement. Je suppose que ma question se pose un peu avant que l'on ait besoin de tout cela, cependant. Ce que j'aimerais vraiment savoir, ce sont les "Idiomes pour le mappage de la conception sur les types/fonctions/classes/monades" ... Je pourrais essayer d'inventer les miens, mais j'espérais qu'il pourrait y avoir un ensemble de meilleures pratiques distillées quelque part - ou sinon, des recommandations pour un code bien structuré à lire d'un système assez large (par opposition à, disons, une bibliothèque ciblée). J'ai modifié mon message pour poser cette même question plus directement.

6 votes

J'ai ajouté du texte sur la décomposition de la conception aux modules. Votre but est d'identifier les fonctions logiquement reliées dans des modules qui ont des interfaces référentiellement transparentes avec les autres parties du système, et d'utiliser des types de données purement fonctionnels dès que possible, autant que possible, pour modéliser le monde extérieur en toute sécurité. Le document de conception de xmonad couvre une grande partie de cela : xmonad.wordpress.com/2009/09/09/

0 votes

Merci encore ! Le document de conception de xmonad est exactement ce que je cherchais. Il est temps de lire du code...

117voto

user349653 Points 681

Don vous a donné la plupart des détails ci-dessus, mais voici mes deux centimes d'euros, tirés de la réalisation de programmes étatiques très pointus comme les démons système en Haskell.

  1. Au final, vous vivez dans une pile de monades transformatrices. En bas, il y a IO. Au-dessus, chaque module important (au sens abstrait, pas au sens de module dans un fichier) fait correspondre son état nécessaire à une couche de cette pile. Ainsi, si votre code de connexion à la base de données est caché dans un module, vous l'écrivez pour qu'il soit sur un type MonadReader Connection m => ... -> m ... et ensuite vos fonctions de base de données peuvent toujours obtenir leur connexion sans que les fonctions des autres modules aient à connaître 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 divers sémaphores et mvars pour la résolution du parallélisme et de la synchronisation, une autre vos gestionnaires de fichiers journaux, etc.

  2. Déterminer le traitement des erreurs premièrement . La plus grande faiblesse actuelle de Haskell dans les grands systèmes est la pléthore de méthodes de gestion des erreurs, y compris des méthodes minables comme Maybe (qui est erronée car vous ne pouvez pas renvoyer d'informations sur ce qui s'est mal passé ; utilisez toujours Either au lieu de Maybe, sauf si vous voulez vraiment parler de valeurs manquantes). Déterminez d'abord comment vous allez procéder, et mettez en place des adaptateurs entre les différents mécanismes de gestion des erreurs utilisés par vos bibliothèques et d'autres codes et votre mécanisme final. Cela vous épargnera bien des soucis par la suite.

Addendum (extrait des commentaires ; merci à Lii & liminalisht ) -
plus de discussion sur les différentes manières de découper un grand programme en monades dans une pile :

Ben Kolera donne une excellente introduction pratique à ce sujet, et Brian Hurt examine les solutions au problème de lift d'actions monadiques 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 qu'avec votre type de monade personnalisé. Carlo Hamalainen a écrit quelques notes courtes et utiles résumant la conférence de George.

5 votes

Deux bons points ! Cette réponse a le mérite d'être raisonnablement concrète, ce que les autres ne sont pas. Il serait intéressant de lire plus de discussions sur les différentes façons de découper un grand programme en monades dans une pile. Veuillez poster des liens vers de tels articles si vous en avez !

6 votes

@Lii Ben Kolera donne une excellente introduction pratique à ce sujet, et Brian Hurt examine les solutions au problème de lift d'actions monadiques 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 qu'avec votre type de monade personnalisé. Carlo Hamalainen a écrit quelques notes courtes et utiles résumant la conférence de George.

0 votes

Je suis d'accord pour dire que les piles de transformateurs de monades ont tendance à être des fondations architecturales clés, mais j'essaie très fort de garder l'IO en dehors de celles-ci. Ce n'est pas toujours possible, mais si vous réfléchissez à ce que signifie "and then" dans votre monade, vous découvrirez peut-être que vous avez vraiment une continuation ou un automate quelque part en bas qui peut ensuite être interprété en IO par une fonction "run".

43voto

augustss Points 402

Concevoir de grands programmes en Haskell n'est pas si différent de le faire dans d'autres langages. La programmation dans les grands programmes consiste à décomposer votre problème en éléments gérables et à les assembler ; le langage d'implémentation est moins important.

Cela dit, dans une grande conception, il est bon d'essayer de tirer parti du système de types pour s'assurer que vous ne pouvez assembler vos pièces que d'une manière correcte. Cela peut impliquer un nouveau type ou des types fantômes pour que des choses qui semblent avoir le même type soient différentes.

Lorsqu'il s'agit de remanier le code au fur et à mesure, la pureté est un atout majeur, alors essayez de garder la plus grande partie possible du code pur. Le code pur est facile à remanier, car il n'a pas d'interaction cachée avec d'autres parties de votre programme.

14 votes

En fait, j'ai trouvé que le remaniement est assez frustrant, si les types de données doivent changer. Cela nécessite de modifier fastidieusement l'arité de beaucoup de constructeurs et de pattern-matches. (Je suis d'accord pour dire que le refactoring de fonctions pures en d'autres fonctions pures du même type est facile - tant que l'on ne touche pas aux types de données).

2 votes

@Dan Vous pouvez vous en sortir complètement gratuitement avec de petites modifications (comme l'ajout d'un champ) lorsque vous utilisez des enregistrements. Certains peuvent vouloir faire des enregistrements une habitude (je suis l'un d'entre eux ^^").

5 votes

@Dan Je veux dire que si vous changez le type de données d'une fonction dans n'importe quel langage, ne devez-vous pas faire de même ? Je ne vois pas comment un langage tel que Java ou C++ pourrait vous aider à cet égard. Si vous dites que vous pouvez utiliser une sorte d'interface commune à laquelle les deux types obéissent, alors vous auriez dû le faire avec les classes de type en Haskell.

16voto

comonad Points 1852

J'ai appris structuré la programmation fonctionnelle 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 présenter des structures clairement organisées.

L'art de la programmation fonctionnelle

The Craft of Functional Programming

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

11 votes

Aussi formidable que soit l'Artisanat de la FP -- j'ai appris Haskell grâce à lui -- c'est une texte d'introduction para programmeurs débutants mais pas pour la conception de grands systèmes en Haskell.

3 votes

C'est le meilleur livre que je connaisse sur la conception d'API et la dissimulation des détails de mise en œuvre. Grâce à ce livre, je suis devenu un meilleur programmeur en C++ - simplement parce que j'ai appris de meilleures façons d'organiser mon code. Eh bien, votre expérience (et votre réponse) est sûrement meilleure que ce livre, mais Dan pourrait probablement être encore un débutant en Haskell. ( where beginner=do write $ tutorials `about` Monads )

15voto

Neil Mitchell Points 2510

J'ai écrit un peu sur ce sujet dans la rubrique "Lignes directrices de conception" de ce document :

http://community.haskell.org/~ndm/downloads/paper-hoogle_overview-19_nov_2008.pdf

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X