Design à grande échelle à Haskell?

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

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

    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 de garantir l’absence de mémoire inutile.

    Pureté

    • Réduisez considérablement la complexité en supprimant l’état. Échelle de code purement fonctionnelle, car elle est compositionnelle. Tout ce dont vous avez besoin est le type pour déterminer comment utiliser du code – il ne sera pas mystérieusement interrompu lorsque vous modifierez une autre partie du programme.
    • Utilisez beaucoup de programmation de style “modèle / vue / contrôleur”: parsingz dès que possible les données externes dans des structures de données purement fonctionnelles, opérez sur ces structures, puis une fois que tout le travail est terminé, restituez / vidangez / sérialisez. Garde la plupart de votre code pur

    Essai

    • QuickCheck + Haskell Code Coverage, pour vous assurer que vous testez les éléments 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 à définir, elles sont probablement trop complexes. Continuez à refactoriser jusqu’à ce que vous ayez un jeu de propriétés propre capable de tester votre code, qui compose bien. Alors le code est probablement bien conçu aussi.

    Monades pour la structuration

    • Les monades capturent les principales conceptions architecturales par types (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 pour quel état est visible à quels composants du système.

    Classes de types et types existentiels

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

    Concurrence et parallélisme

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

    Refactor

    • Vous pouvez refactoriser dans Haskell beaucoup . Les types garantissent la sécurité de vos modifications à grande échelle, si vous utilisez judicieusement les types. Cela aidera votre échelle de base de code. Assurez-vous que vos refactorings provoquent des erreurs de type jusqu’à la fin.

    Utilisez le FFI à bon escient

    • Le FFI facilite la lecture du code étranger, mais ce code étranger peut être dangereux.
    • Soyez très prudent dans les hypothèses sur la forme des données renvoyées.

    Meta programmation

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

    Emballage et dissortingbution

    • Utilisez Cabal. Ne lancez pas votre propre système de construction. (EDIT: En fait, vous voudrez probablement utiliser Stack maintenant pour commencer.)
    • Utiliser Haddock pour de bons documents API
    • Des outils tels que graphmod peuvent afficher les structures de vos modules.
    • Faites confiance aux versions de bibliothèques et d’outils de la plate-forme Haskell, si possible. C’est une base stable. (EDIT: Encore une fois, ces jours-ci, vous voudrez probablement utiliser Stack pour obtenir une base stable et opérationnelle.)

    Avertissements

    • Utilisez -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.

    1. 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.

    2. 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

    Le métier de la programmation fonctionnelle

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

    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:

    • Approches de la modélisation de l’architecture à l’aide de diagrammes;
    • Analyse des besoins;
    • Modélisation de domaine DSL intégrée;
    • Conception et implémentation de DSL externe;
    • Monades en tant que sous-systèmes à effets;
    • Monades libres comme interfaces fonctionnelles;
    • EDSL sous forme de flèche;
    • Inversion du contrôle en utilisant des eDSL monadiques libres;
    • Mémoire transactionnelle logicielle;
    • Lentilles;
    • État, lecteur, écrivain, RWS, ST monades;
    • État impur: IORef, MVar, STM;
    • La modélisation multithreading et simultanée de domaines;
    • 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 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.