Question Conception à grande échelle à Haskell? [fermé]


Quel est un bon moyen de concevoir / structurer de grands programmes fonctionnels, en particulier chez Haskell?

J'ai suivi une série de tutoriels (Ecrivez-vous un régime étant mon préféré, avec Real World Haskell en second lieu) - mais la plupart des programmes sont relativement petits et à usage unique. De plus, je ne considère pas certains d'entre eux comme particulièrement élégants (par exemple, les vastes tables de recherche de WYAS).

Je veux maintenant écrire des programmes plus grands, avec plus de parties mobiles - acquérir des données à partir de différentes sources, les nettoyer, les traiter de différentes manières, les afficher dans des interfaces utilisateur, les persister, communiquer sur des réseaux, etc. une meilleure structuration de ce code pour qu'il soit lisible, maintenable et adaptable aux exigences changeantes?

Il existe une littérature assez vaste 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 réaliser des objectifs généraux tels que 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 refactorisation «à mesure que vous grandissez» auquel, selon mon opinion novice, Haskell semble moins bien convenir.

Existe-t-il une littérature équivalente pour Haskell? Comment le zoo des structures de contrôle exotiques est-il disponible dans la programmation fonctionnelle (monades, flèches, applicatifs, etc.) le mieux utilisé à cette fin? Quelles meilleures pratiques pourriez-vous recommander?

Merci!

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

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

Je suppose que ma question est: comment devrait-on penser à des conceptions architecturales clés 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 modulaires pour les flux de données à un ensemble de structures de données, et je peux mettre en œuvre chaque étape de traitement comme une fonction pure. Les étapes de traitement requises pour une donnée dépendent de sa valeur et de celle des autres. Certaines é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 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 peut 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 tout ce dont elle a besoin à partir de l'état de monade. Ou on pourrait écrire des programmes en grande partie séparés et envoyer des messages (je n'aime pas beaucoup cette option).

Les diapositives qu'il a liées ont une puce Choses dont nous avons besoin: "Idiomes pour la conception de la cartographie sur types / fonctions / classes / monades ".Quels sont les idiomes? :)


552
2018-06-20 01:21


origine


Réponses:


Je parle un peu à ce sujet dans Grands projets d'ingénierie à Haskell et dans le Conception et mise en œuvre de XMonad. L'ingénierie dans le grand est sur la gestion de la complexité. Les principaux mécanismes de structuration du code dans Haskell pour gérer la complexité sont:

Le système de type

  • Utilisez le système de type pour appliquer les abstractions, ce qui simplifie les interactions.
  • Appliquer les invariants clés via les types
    • (Par exemple, certaines valeurs ne peuvent pas échapper à une certaine portée)
    • Ce certain code ne fait pas d'E / S, ne touche pas le disque
  • Appliquer la sécurité: vérifier les exceptions (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 inutiles certaines classes de tests, car elles excluent par ex. erreurs hors limites statiquement.

Le profileur

  • Fournissez des preuves objectives des profils de tas et de temps de votre programme.
  • Le profilage en 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. Échelles de code purement fonctionnelles, parce que c'est la composition. Tout ce dont vous avez besoin est le type pour déterminer comment utiliser du code - il ne se casse pas mystérieusement lorsque vous changez une autre partie du programme.
  • Utiliser beaucoup de programmation de type "modèle / vue / contrôleur": analyser les données externes le plus rapidement possible en structures de données purement fonctionnelles, opérer sur ces structures, puis, une fois que tout le travail est terminé, rendre / vider / sérialiser. Conserve la majeure partie de votre code pur

Essai

  • QuickCheck + Haskell Code Coverage, pour vous 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. Gardez refactoring jusqu'à ce que vous ayez un ensemble propre de propriétés qui peuvent 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 des 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 le design pour quel état est visible à quels composants du système.

Classes de types et types existentiels

  • Utilisez les classes de types pour fournir l'abstraction: masquez les implémentations derrière les interfaces polymorphes.

Concurrence et parallélisme

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

Refactor

  • Vous pouvez refactoriser à Haskell beaucoup. Les types garantissent que vos changements à grande échelle seront sûrs, si vous utilisez les types à bon escient. Cela aidera votre échelle de base de code. Assurez-vous que vos refactorings provoqueront des erreurs de type jusqu'à la fin.

Utilisez le FFI avec sagesse 

  • Le FFI le rend plus facile à jouer avec du code étranger, mais ce code étranger peut être dangereux.
  • Soyez très prudent dans les hypothèses sur la forme des données retournées.

Meta programmation

  • Un peu de modèle Haskell ou génériques peut supprimer la plaque de cuisson.

Emballage et distribution

  • Utilisez Cabal. Ne lancez pas votre propre système de construction. (EDIT: En fait, vous voulez probablement utiliser Empiler maintenant pour commencer.).
  • Utilisez Haddock pour de bons documents API
  • Des outils comme graphmod peut montrer vos structures de module.
  • S'appuyer sur les versions de la plate-forme Haskell des bibliothèques et des outils, si possible. C'est une base stable. (EDIT: Encore une fois, ces jours, vous voulez probablement utiliser Empiler pour obtenir une base stable et opérationnelle.)

Avertissements

  • Utilisation -Wall pour garder votre code propre des odeurs. Vous pouvez également regarder Agda, Isabelle ou Catch pour plus d'assurance. Pour le contrôle de la peluche, voyez le bon hlint, ce qui va suggérer des améliorations.

Avec tous ces outils, vous pouvez gérer la complexité en supprimant autant d'interactions entre les composants que possible. Idéalement, vous avez une très grande base de code pur, ce qui est vraiment facile à maintenir, car c'est de la composition. Ce n'est pas toujours possible, mais cela vaut la peine d'être visé.

En général: décomposer les unités logiques de votre système dans les plus petits composants référentiellement transparents possibles, puis les implémenter dans des modules. Les environnements globaux ou locaux pour des ensembles de composants (ou de composants internes) peuvent être mappés à 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.


510
2018-06-20 01:42



Don vous a donné la plupart des détails ci-dessus, mais voici mes deux cents de faire des programmes stateful vraiment nitty-gravely comme les démons du système dans Haskell.

  1. À la fin, vous vivez dans une pile de transformateur de monade. Au fond est IO. Au-dessus, chaque module majeur (au sens abstrait, pas au sens du module dans un fichier) mappe son état nécessaire en une couche dans cette pile. Donc, si vous avez votre code de connexion de base de données caché dans un module, vous écrivez tout pour être sur un type MonadReader Connexion m => ... -> m ... et puis vos fonctions de base de données peuvent toujours obtenir leur connexion sans d'autres fonctions modules devant être conscients de son existence. Vous pourriez vous retrouver avec une couche transportant votre connexion à la base de données, une autre votre configuration, une troisième vos sémaphores et mvars pour la résolution du parallélisme et de la synchronisation, une autre pour le fichier journal, etc.

  2. Découvrez votre gestion des erreurs premier. La plus grande faiblesse en ce moment pour Haskell dans les plus grands systèmes est la pléthore de méthodes de gestion des erreurs, y compris les erreurs comme Maybe (ce qui est faux car vous ne pouvez pas retourner les informations sur ce qui s'est mal passé. juste signifier des valeurs manquantes). Déterminez comment vous allez le faire en premier et configurez les adaptateurs des différents 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.

Addenda (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 grande introduction pratique à ce sujet, et Brian Hurt discute des solutions au problème de liftdes 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 type requises, plutôt que votre type de monade personnalisé. Carlo Hamalainen a écrit quelques notes courtes et utiles résumant le discours de George.


117
2018-06-21 10:39



Concevoir de grands programmes dans Haskell n'est pas si différent de le faire dans d'autres langues. La programmation dans le grand est de briser votre problème en morceaux gérables, et comment les adapter ensemble; le langage de mise en œuvre est moins important.

Cela dit, dans un grand design, 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 newtype ou des types fantômes pour faire des choses qui semblent avoir le même type être différent.

Quand il s'agit 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 aucune interaction cachée avec d'autres parties de votre programme.


43
2018-06-20 09:29



J'ai appris structuré programmation fonctionnelle la première fois avec ce livre. Ce n'est peut-être pas exactement ce que vous cherchez, 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. Sur tous les niveaux d'abstraction, le design doit toujours avoir des structures clairement arrangées.

L'artisanat de la programmation fonctionnelle

The Craft of Functional Programming

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


16
2017-10-17 23:23



Je suis en train d'écrire un livre intitulé "Functional Design and Architecture". Il vous fournit un ensemble complet de techniques pour construire une grande application en utilisant une approche purement fonctionnelle. Il décrit de nombreux modèles fonctionnels et des idées tout en construisant une application similaire à 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 d'architecture à l'aide de diagrammes;
  • Analyse des besoins;
  • Modélisation de domaine DSL intégrée;
  • Conception et implémentation DSL externe;
  • Les monades en tant que sous-systèmes avec effets;
  • Monades libres en tant qu'interfaces fonctionnelles;
  • EDSL Arrowized;
  • Inversion de contrôle en utilisant des eDSL monadiques gratuits;
  • Mémoire transactionnelle de logiciel;
  • Lentilles;
  • Etat, lecteur, écrivain, RWS, ST monades;
  • Etat impur: IORef, MVar, STM;
  • Multithreading et modélisation de domaine simultanée;
  • GUI;
  • 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 'Andromède' code de projet.

Je m'attends à finir ce livre à la fin de l'année 2017. Jusqu'à ce que cela se produise, vous pouvez lire mon article "Design and Architecture in Functional Programming" (Rus) ici.

METTRE À JOUR

J'ai partagé mon livre en ligne (5 premiers chapitres). Voir publier sur Reddit


11
2017-11-20 05:25



Le blogue de Gabriel Architectures de programme évolutives pourrait valoir une mention.

Les modèles de conception Haskell diffèrent des modèles de conception traditionnels en un   manière importante:

  • Architecture conventionnelle: Combiner plusieurs composants ensemble de   tapez A pour générer un "réseau" ou une "topologie" de type B

  • L'architecture Haskell: Combiner plusieurs composants ensemble de type A à   générer un nouveau composant du même type A, indiscernable   caractère de ses parties substituantes

Il me vient souvent à l'esprit qu'une architecture apparemment élégante tend souvent à sortir des bibliothèques qui présentent ce sens de l'homogénéité, de manière ascendante. Dans Haskell, ceci est particulièrement apparent - les modèles qui seraient traditionnellement considérés comme une architecture descendante ont tendance à être capturés dans des bibliothèques comme mvc, Netwire et Nuage Haskell. C'est-à-dire, j'espère que cette réponse ne sera pas interprétée comme une tentative de remplacer tous les autres dans ce fil, juste que les choix structurels peuvent et devraient idéalement être abstraits dans les bibliothèques par des experts du domaine. Selon moi, la véritable difficulté à construire de grands systèmes est d'évaluer ces bibliothèques sur leur «bonté» architecturale par rapport à toutes vos préoccupations pragmatiques.

Comme liminalisht mentionne dans les commentaires, Le modèle de conception de catégorie est un autre message de Gabriel sur le sujet, dans la même veine.


7
2017-08-31 10:07



J'ai trouvé le papier "Enseignement de l'architecture logicielle avec Haskell"(pdf) par Alejandro Serrano utile pour penser à la structure à grande échelle à Haskell.


5
2018-02-18 19:24



Vous devez peut-être prendre du recul et réfléchir à la façon de traduire la description du problème en un design. Comme Haskell est un niveau aussi élevé, 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, parce que vous effectuez par exemple une base de données Access à partir d'une bibliothèque. Et voilà, il y a le programme. Le compilateur nourrit 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 «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 est de créer une bibliothèque qui résume un ensemble de problèmes connexes.


3