Quelle est une bonne façon de concevoir / structurer de grands programmes fonctionnels, en particulier dans Haskell?
J'ai parcouru un tas de tutoriels (Write Yourself a Scheme étant mon préféré, avec Real World Haskell juste après) - 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 recherche dans WYAS).
Je veux maintenant écrire des programmes plus grands, 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 différentes manières, les afficher dans les interfaces utilisateur, les conserver, communiquer sur les réseaux, etc. une meilleure structure de ce code pour être 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. Des idées comme MVC, des modèles de conception, etc. sont des prescriptions décentes pour atteindre des objectifs généraux tels que la séparation des préoccupations et la réutilisabilité dans un style OO. De plus, les nouveaux langages impératifs se prêtent à un style de refactoring de conception à mesure que vous grandissez auquel, à mon avis novice, Haskell semble moins bien adapté.
Existe-t-il une littérature équivalente pour Haskell? Comment le zoo des structures de contrôle exotiques disponibles en programmation fonctionnelle (monades, flèches, applicatives, etc.) est-il le mieux employé à cet effet? Quelles meilleures pratiques pourriez-vous recommander?
Merci!
EDIT (ceci fait suite à la réponse de Don Stewart):
@dons a mentionné: "Les monades capturent les conceptions architecturales clés en types."
Je suppose que ma question est: comment penser les 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 dans 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 une donnée dépendront de sa valeur et des autres. Certaines étapes doivent être suivies d'effets secondaires tels que les mises à jour de l'interface graphique ou les requêtes de base de données.
Quelle est la «bonne» façon de lier les données et les étapes d'analyse d'une manière agréable? On pourrait écrire une grosse fonction qui fait ce qu'il faut 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 que chaque étape de traitement obtienne tout ce dont elle a besoin à partir de l'état de la monade. Ou 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: "Idioms for mapping design on types / functions / classes / monads". Quels sont les idiomes? :)
Réponses:
J'en parle un peu dans Engineering Large Projects à Haskell et dans la conception et la mise en œuvre de XMonad. L'ingénierie dans son ensemble consiste à gérer la complexité. Les principaux mécanismes de structuration de code dans Haskell pour gérer la complexité sont:
Le système de type
Le profileur
Pureté
Essai
Monades pour structurer
Classes de types et types existentiels
Concurrence et parallélisme
par
vous dans votre programme pour battre la concurrence avec un parallélisme facile à composer.Refactor
Utilisez le FFI à bon escient
Programmation méta
Emballage et distribution
Avertissements
-Wall
pour garder votre code propre des odeurs. Vous pouvez également consulter Agda, Isabelle ou Catch pour plus d'assurance. Pour une vérification semblable à des peluches, consultez le grand hlint , qui suggérera des améliorations.Avec tous ces outils, vous pouvez maîtriser la complexité, en supprimant autant d'interactions entre les composants que possible. Idéalement, vous disposez d'une très grande base de code pur, qui est vraiment facile à maintenir, car il est de composition. 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 les plus petits composants référentiellement transparents possibles, puis implémentez-les en modules. Les environnements globaux ou locaux pour les ensembles de composants (ou à l'intérieur des composants) peuvent être mappés sur des monades. Utilisez des types de données algébriques pour décrire les structures de données principales. Partagez largement ces définitions.
la source
Don vous a donné la plupart des détails ci-dessus, mais voici mes deux cents de faire des programmes avec état vraiment nitty-graveleux comme les démons système dans Haskell.
À la fin, vous vivez dans une pile de transformateurs monade. En bas se trouve IO. Au-dessus de cela, chaque module majeur (dans le sens abstrait, pas le sens module dans un fichier) mappe son état nécessaire dans une couche de cette pile. Donc, si vous avez votre code de connexion à la base de données caché dans un module, vous écrivez tout pour être sur un type de connexion MonadReader m => ... -> m ... et vos fonctions de base de données peuvent toujours obtenir leur connexion sans fonctions d'autres 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 que votre fichier journal gère, etc.
Calculez votre erreur de manipulation d' abord . La plus grande faiblesse à l'heure actuelle pour Haskell dans les systèmes plus grands est la pléthore de méthodes de gestion des erreurs, y compris les moche comme Peut-être (ce qui est faux parce que vous ne pouvez pas retourner d'informations sur ce qui s'est mal passé; utilisez toujours Soit au lieu de Peut-être, sauf si vous avez vraiment signifie simplement des valeurs manquantes). Découvrez comment vous allez le faire en premier et configurez des adaptateurs à partir des divers mécanismes de gestion 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; merci à Lii & liminalisht ) -
plus de discussion sur les différentes façons de découper un grand programme en monades dans une pile:
Ben Kolera donne une excellente introduction pratique à ce sujet, et Brian Hurt discute des solutions au problème de l'
lift
intégration d'actions monadiques dans votre monade personnalisée. George Wilson montre comment utilisermtl
pour écrire du code qui fonctionne avec n'importe quelle monade qui implémente les classes de caractères requises, plutôt que votre type de monade personnalisé. Carlo Hamalainen a écrit quelques notes courtes et utiles résumant le discours de George.la source
lift
intégration d'actions monadiques dans votre monade personnalisée. George Wilson montre comment utilisermtl
pour écrire du code qui fonctionne avec n'importe quelle monade qui implémente les classes de caractères requises, plutôt que votre type de monade personnalisé. Carlo Hamalainen a écrit quelques notes courtes et utiles résumant le discours de George.Concevoir de grands programmes dans Haskell n'est pas si différent de le faire dans d'autres langues. La programmation dans son ensemble consiste à diviser votre problème en éléments gérables et à savoir comment les assembler; le langage d'implémentation est moins important.
Cela dit, dans un grand modèle, il est agréable d'essayer de tirer parti du système de saisie pour vous assurer que vous ne pouvez assembler vos pièces que d'une manière correcte. Cela peut impliquer des types nouveaux ou fantômes pour que les choses qui semblent avoir le même type soient différentes.
Quand il s'agit de refactoriser le code au fur et à mesure, la pureté est une grande aubaine, alors essayez de garder autant de code que possible pur. Le code pur est facile à refactoriser, car il n'a aucune interaction cachée avec d'autres parties de votre programme.
la source
J'ai appris la programmation fonctionnelle structurée 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
http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/
la source
where beginner=do write $ tutorials `about` Monads
)J'écris actuellement un livre intitulé "Design fonctionnel et architecture". Il vous fournit un ensemble complet de techniques pour construire une grande application en utilisant une approche fonctionnelle pure. Il décrit de nombreux modèles et idées fonctionnels lors de la construction d'une application de type SCADA «Andromeda» pour contrôler les vaisseaux spatiaux à partir de zéro. Ma langue principale est le haskell. Le livre couvre:
Vous pouvez vous familiariser avec le code du livre ici , et le code du projet 'Andromeda' .
Je m'attends à terminer ce livre à la fin de 2017. En attendant, vous pouvez lire mon article "Conception et architecture en programmation fonctionnelle" (Rus) ici .
MISE À JOUR
J'ai partagé mon livre en ligne (5 premiers chapitres). Voir le post sur Reddit
la source
Le blog de Gabriel Les architectures de programme évolutives méritent peut-être une mention.
Il me semble souvent qu'une architecture apparemment élégante a souvent tendance à tomber des bibliothèques qui présentent ce joli sens de l'homogénéité, d'une manière ascendante. À Haskell, cela est particulièrement apparent - les modèles qui étaient traditionnellement considérés comme une «architecture descendante» ont tendance à être capturés dans des bibliothèques comme mvc , Netwire et Cloud Haskell. . C'est-à-dire, j'espère que cette réponse ne sera pas interprétée comme une tentative de remplacer l'une des autres dans ce fil, juste que les choix structurels peuvent et doivent idéalement être abstraits dans les bibliothèques par des experts du domaine. À mon avis, la vraie difficulté de construire de grands systèmes est d'évaluer ces bibliothèques sur leur «bonté» architecturale par rapport à toutes vos préoccupations pragmatiques.
Comme le mentionne liminalisht dans les commentaires, Le modèle de conception de catégorie est un autre article de Gabriel sur le sujet, dans la même veine.
la source
J'ai trouvé l'article " Enseigner l'architecture logicielle à l'aide de Haskell " (pdf) par Alejandro Serrano utile pour réfléchir à une structure à grande échelle à Haskell.
la source
Peut-être devez-vous prendre un peu de recul et réfléchir à la façon de traduire la description du problème en conception en premier lieu. Étant donné que Haskell est 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 trouvez des erreurs concrètes sur les champs manquants, les instances manquantes et les transformateurs monadiques manquants dans votre code, car par exemple, vous effectuez un accès à une base de données à partir d'une bibliothèque qui a besoin d'une certaine monade d'état dans une procédure d'E / S. 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 façon, 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 concret ordinaire. Je pense que la suringénierie est la chose la plus dangereuse en informatique. Les choses sont différentes lorsque le problème est de créer une bibliothèque qui résume un ensemble de problèmes connexes.
la source