Quel est le bon moyen de concevoir / structurer de grands programmes fonctionnels, en particulier dans Haskell?
Je suis passé par un tas de tutoriels (Write Yourself a Scheme étant mon préféré, avec Real World Haskell au second plan) – mais la plupart des programmes sont relativement petits et à usage 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 veux maintenant écrire des programmes plus volumineux, avec plus de pièces mobiles – acquérir des données de différentes sources, les nettoyer, les traiter de différentes manières, les afficher dans des interfaces utilisateur, les persister, 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 object. Des idées telles que MVC, les modèles de conception, etc. sont des prescriptions décentes pour réaliser des objectives généraux tels que la séparation des préoccupations et la réutilisation dans un style OO. En outre, les nouveaux langages impératifs se prêtent à un style de refonte de type «conception en cours de développement» auquel, selon moi, Haskell semble moins bien adapté.
Y a-t-il une littérature équivalente pour Haskell? Comment le zoo des structures de contrôle exotiques est-il disponible dans la functional programming (monades, flèches, applicatif, etc.) le mieux utilisé à cette fin? Quelles meilleures pratiques pourriez-vous recommander?
Merci!
EDIT (ceci fait suite à la réponse de Don Stewart):
@dons a mentionné: “Les monades capturent les principales conceptions architecturales par types.”
Je suppose que ma question est la suivante: comment penser les conceptions architecturales clés dans un langage purement fonctionnel?
Prenons l’exemple de plusieurs stream de données et plusieurs étapes de traitement. Je peux écrire des parsingurs modulaires pour les stream 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 requirejses pour une donnée dépendent de sa valeur et de celle des autres. Certaines étapes doivent être suivies d’effets secondaires tels que les mises à jour de l’interface graphique ou les requêtes de firebase database.
Quelle est la bonne façon de lier les données et les étapes d’parsing de manière agréable? On pourrait écrire une grosse fonction qui fait le bon choix pour les différents types de données. Ou on peut utiliser une monade pour garder une trace de ce qui a été traité jusqu’ici et chaque étape de traitement obtient ce dont elle a besoin à partir de l’état 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 associées ont une puce «Things we Need»: «Les idiomes pour la conception de cartes sur les types / fonctions / classes / monades». Quels sont les idiomes? 🙂
Je parle un peu de cela dans Engineering Large Projects à Haskell et dans la conception et la mise en œuvre de XMonad. L’ingénierie au sens large 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 type
Le profileur
Pureté
Essai
Monades pour la structuration
Classes de types et types existentiels
Concurrence et parallélisme
par
dans votre programme pour battre la concurrence avec un parallélisme facile et composable. Refactor
Utilisez le FFI à bon escient
Meta programmation
Emballage et dissortingbution
Avertissements
-Wall
pour garder votre code propre des odeurs. Vous pouvez également regarder Agda, Isabelle ou Catch pour plus d’assurance. Pour une vérification similaire à celle des peluches, voir le grand graphique , qui suggérera des améliorations. Avec tous ces outils, vous pouvez garder la maîsortingse de la complexité, en supprimant autant d’interactions que possible entre les composants. Idéalement, vous avez une base de code pure très large, ce qui est vraiment facile à gérer, car elle est compositionnelle. Ce n’est pas toujours possible, mais cela vaut la peine d’être recherché.
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 dans des modules. Les environnements globaux ou locaux pour des ensembles de composants (ou des composants internes) peuvent être mappés sur des monades. Utilisez des types de données algébriques pour décrire les structures de données de base. Partagez ces définitions largement.
Don vous a donné la plupart des détails ci-dessus, mais voici mes deux centimes de faire des programmes avec un peu de complication, comme des démons système dans Haskell.
En fin de compte, vous vivez dans une stack de transformateurs monade. Au bas est IO. Au-dessus de cela, chaque module majeur (au sens abstrait, et non le sens module-dans-un-fichier) mappe son état nécessaire dans une couche de cette stack. Donc si vous avez votre code de connexion de firebase database caché dans un module, vous écrivez tout pour être sur un type MonadReader Connection m => … -> m … et alors vos fonctions de firebase database 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 firebase database, 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 traite, etc.
Déterminez votre gestion des erreurs en premier . La plus grande faiblesse en ce moment pour Haskell dans les grands systèmes est la pléthore de méthodes de traitement des erreurs, y compris les plus mauvaises comme Maybe (ce qui est faux car vous ne pouvez pas renvoyer d’informations) signifie simplement 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 système final. Cela vous sauvera un monde de chagrin plus tard.
Addendum (extrait de commentaires; merci à Lii & liminalisht ) –
Plus de discussion sur les différentes façons de découper un grand programme en monades dans une stack:
Ben Kolera donne une introduction pratique à ce sujet et Brian Hurt discute des solutions au problème de l’introduction d’actions monadiques dans votre monade personnalisée. George Wilson montre comment utiliser mtl
pour écrire du code qui fonctionne avec tous les monad qui implémentent les classes de caractères requirejses, plutôt que votre type monad personnalisé. Carlo Hamalainen a écrit de courtes notes 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 à les adapter à la réalité. le langage de mise en œuvre est moins important.
Cela dit, dans une conception de grande taille, il est intéressant d’essayer de tirer parti du système de caractères 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 rendre les choses qui semblent avoir le même type être différentes.
En matière de refactorisation du code au fur et à mesure, la pureté est un avantage, alors essayez de garder le plus de code possible. Le code pur est facile à restructurer, car il n’a pas d’interaction cachée avec d’autres parties de votre programme.
J’ai appris la functional programming 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 functional programming, cela peut être l’une des meilleures premières étapes pour apprendre à structurer des programmes fonctionnels – indépendamment de l’échelle. Sur tous les niveaux d’abstraction, le design doit toujours avoir des structures clairement agencées.
Le métier de la functional programming
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 fonctionnelle pure. Il décrit de nombreux modèles et idées fonctionnels tout en créant une application de type SCADA «Andromeda» pour contrôler les vaisseaux spatiaux à partir de zéro. Ma langue principale est Haskell. Le livre couvre:
Vous pouvez vous familiariser avec le code du livre ici et le code du projet «Andromeda» .
Je m’attends à finir ce livre à la fin de 2017. En attendant, vous pouvez lire ici mon article “Design et architecture en functional programming” (Rus).
METTRE À JOUR
J’ai partagé mon livre en ligne (5 premiers chapitres). Voir l’ article sur Reddit
Le post du blog de Gabriel Les architectures de programme évolutives méritent d’être mentionnées.
Les modèles de conception Haskell diffèrent 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 de Haskell : combine 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 apparemment élégante a souvent tendance à tomber des bibliothèques qui présentent ce beau sentiment d’homogénéité, de manière ascendante. Dans Haskell, ceci est particulièrement apparent – les patterns qui seraient 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 que j’espère que cette réponse ne sera pas interprétée comme une tentative de remplacement des autres éléments de ce fil, mais que les experts du domaine peuvent et devraient idéalement séparer les choix structurels dans les bibliothèques. À mon avis, la véritable difficulté à créer de grands systèmes consiste à évaluer ces bibliothèques en fonction de 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.
J’ai trouvé le papier ” L’enseignement de l’architecture logicielle utilisant Haskell ” (pdf) d’Alejandro Serrano utile pour réfléchir à la structure à grande échelle dans Haskell.
Peut-être devez-vous faire un pas en arrière et réfléchir à la façon de traduire la description du problème en une conception en premier lieu. Puisque Haskell est tellement 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 comstackz ce code et trouvez des erreurs concrètes sur les champs manquants, les instances manquantes et les transformateurs monadiques manquants dans votre code, car vous effectuez par exemple une firebase database Access à partir d’une bibliothèque nécessitant une certaine monade d’état dans une procédure IO. Et voilà, il y a le programme. Le compilateur alimente vos esquisses mentales et donne de la 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 ordinaire concret. Je pense que l’ingénierie excessive 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.