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? :)
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
Le profileur
Pureté
Test
Monades pour structurer
Classes de types et types existentiels
Concurrence et parallélisme
par
dans votre programme pour vaincre la concurrence avec un parallélisme facile et composable.Refactor
Utilisez le FFI à bon escient
Méta-programmation
Emballage et distribution
Avertissements
-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.
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.
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.
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.
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.
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
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:
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
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.
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.
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.