web-dev-qa-db-fra.com

Quel est le problème avec Template Haskell?

Il semble que Template Haskell soit souvent considéré par la communauté de Haskell comme une commodité regrettable. Il est difficile de décrire avec exactitude ce que j’ai observé à cet égard, mais considérons ces quelques exemples.

J'ai vu plusieurs articles de blog où les gens font de jolies choses avec Template Haskell, permettant une syntaxe plus jolie qui ne serait tout simplement pas possible dans Haskell classique, ainsi qu'une réduction considérable du nombre de passe-partout. Alors pourquoi est-ce que Template Haskell est méprisé de cette manière? Qu'est-ce qui le rend indésirable? Dans quelles circonstances Template Haskell doit-il être évité et pourquoi?

248
Dan Burton

Une des raisons pour éviter Template Haskell est que, dans son ensemble, il n’est pas du tout sûr, il va donc à l’encontre de "l’esprit de Haskell". Voici quelques exemples:

  • Vous n'avez aucun contrôle sur le type de code Haskell AST qu'un morceau de code TH va générer, au-delà de l'endroit où il apparaîtra; vous pouvez avoir une valeur de type Exp , mais vous ne savez pas s'il s'agit d'une expression représentant un [Char] ou un (a -> (forall b . b -> c)) ou peu importe. TH serait plus fiable si l'on pouvait exprimer le fait qu'une fonction ne peut générer que des expressions d'un certain type, ou uniquement des déclarations de fonction, ou uniquement des modèles de correspondance de constructeur de données, etc.
  • Vous pouvez générer des expressions non compilées. Vous avez généré une expression faisant référence à une variable libre foo qui n'existe pas? Pas de chance, vous ne le verrez que lorsque vous utilisez réellement votre générateur de code, et uniquement dans les circonstances qui déclenchent la génération de ce code particulier. Il est également très difficile d'effectuer des tests unitaires.

TH est aussi carrément dangereux:

  • Le code exécuté au moment de la compilation peut effectuer des opérations arbitraires IO, notamment le lancement de missiles ou le vol de votre carte de crédit. Vous ne voulez pas avoir à parcourir tous les paquets de cabales que vous avez téléchargés à la recherche d'exploits TH.
  • TH peut accéder à des fonctions et à des définitions "de modules privés", ce qui casse totalement l’encapsulation.

Il existe ensuite quelques problèmes qui rendent les fonctions de TH moins amusantes à utiliser en tant que développeur de bibliothèque:

  • Le code TH n'est pas toujours composable. Supposons que quelqu'un fabrique un générateur de lentilles, et le plus souvent, ce générateur sera structuré de manière à ce qu'il ne puisse être appelé directement que par "l'utilisateur final" et non par un autre code TH, en prenant par exemple une liste de constructeurs de types pour générer des lentilles comme paramètre. Il est difficile de générer cette liste en code, alors que l'utilisateur n'a qu'à écrire generateLenses [''Foo, ''Bar].
  • Les développeurs ne savent même pas que le code TH peut être composé. Saviez-vous que vous pouvez écrire forM_ [''Foo, ''Bar] generateLens? Q n'est qu'un monade, vous pouvez donc utiliser toutes les fonctions habituelles. Certaines personnes ne le savent pas et, à cause de cela, elles créent plusieurs versions surchargées de fonctions essentiellement identiques avec les mêmes fonctionnalités, et ces fonctions entraînent un certain effet de gonflement. En outre, la plupart des gens écrivent leurs générateurs dans la monade Q même quand ils ne sont pas obligés de le faire, ce qui revient à écrire bla :: IO Int; bla = return 3; vous donnez à une fonction plus "d'environnement" que nécessaire, et les clients de la fonction doivent fournir cet environnement en conséquence.

Enfin, certains éléments rendent les fonctions TH moins amusantes à utiliser en tant qu'utilisateur final:

  • Opacité. Quand une fonction TH a le type Q Dec, Elle peut absolument tout générer au plus haut niveau d'un module et vous n'avez absolument aucun contrôle sur ce qui sera généré.
  • Monolithisme. Vous ne pouvez pas contrôler la quantité générée par une fonction TH à moins que le développeur ne l'autorise. si vous trouvez une fonction qui génère une interface de base de données et une interface de sérialisation JSON, vous ne pouvez pas dire "Non, je ne veux que l'interface de base de données, merci Je vais lancer ma propre interface JSON "
  • Temps d'exécution. Le code TH prend relativement longtemps à s'exécuter. Le code est interprété de nouveau à chaque fois qu'un fichier est compilé, et souvent, une tonne de paquets est requise par le code en cours d'exécution TH, qui doit être chargé. Cela ralentit considérablement le temps de compilation.
169
dflemstr

Ceci est uniquement ma propre opinion.

  • C'est moche à utiliser. $(fooBar ''Asdf) n'a tout simplement pas l'air bien. Superficiel, certes, mais cela contribue.

  • C'est encore plus laid d'écrire. La citation fonctionne parfois, mais vous devez souvent effectuer manuellement la manipulation AST) et la plomberie. Le API est énorme et difficile à manier, il y a toujours beaucoup de cas qui ne vous intéressent pas mais dont vous avez toujours besoin d'envoyer, et les cas qui vous intéressent ont tendance à être présents sous plusieurs formes similaires mais non identiques (données vs nouveau type, style d'enregistrement vs constructeurs normaux, et cetera). Il est ennuyeux et répétitif d’écrire et assez compliqué pour ne pas être mécanique. Le proposition de réforme résout certains de ces problèmes (en rendant les citations plus largement applicables).

  • La restriction de l'étape est l'enfer. Ne pas pouvoir épisser les fonctions définies dans le même module en est la partie la plus petite: l'autre conséquence est que si vous avez une épissure de niveau supérieur, tout ce qui le suit dans le module sera hors de portée par rapport à ce qu'il était avant. D'autres langages avec cette propriété (C, C++) le rendent utilisable en vous permettant de transmettre des déclarations, mais pas Haskell. Si vous avez besoin de références cycliques entre des déclarations épissées ou leurs dépendances et leurs dépendants, vous êtes généralement simplement foutu.

  • C'est indiscipliné. Ce que je veux dire par là, c'est que la plupart du temps, lorsque vous exprimez une abstraction, il existe une sorte de principe ou de concept derrière cette abstraction. Pour beaucoup d'abstractions, le principe qui les sous-tend peut être exprimé dans leurs types. Pour les classes de types, vous pouvez souvent formuler des lois auxquelles les instances doivent obéir et que les clients peuvent assumer. Si vous utilisez nouvelle fonctionnalité générique de GHC pour résumer la forme d'une déclaration d'instance sur tout type de données (dans les limites), vous obtenez "pour les types sum, cela fonctionne comme ceci, pour les types de produit, cela fonctionne comme ça". Template Haskell, en revanche, n'est que des macros. Ce n'est pas une abstraction au niveau des idées, mais une abstraction au niveau des AST, ce qui est mieux, mais modestement, que l'abstraction au niveau du texte brut. *

  • Cela vous lie à GHC. En théorie, un autre compilateur pourrait l'implémenter, mais en pratique, je doute que cela se produise un jour. (Cela contraste avec diverses extensions de systèmes de types qui, même si elles ne sont actuellement mises en œuvre que par GHC, sont faciles à imaginer en train d’être adoptées par d’autres compilateurs et finalement normalisées.)

  • L'API n'est pas stable. Lorsque de nouvelles fonctionnalités linguistiques sont ajoutées à GHC et que le paquet template-haskell est mis à jour pour les prendre en charge, cela implique souvent des modifications incompatibles avec les versions antérieures de TH types de données. Si vous souhaitez que votre TH soit compatible avec plusieurs versions de GHC, vous devez faire très attention et utiliser éventuellement CPP.

  • Il existe un principe général selon lequel vous devez utiliser le bon outil pour le travail et le plus petit qui suffira, et dans cette analogie, Template Haskell est quelque chose comme cela . S'il existe un moyen de le faire qui ne soit pas Template Haskell, c'est généralement préférable.

L'avantage de Template Haskell est que vous pouvez faire des choses avec ce que vous ne pourriez pas faire autrement, et c'est un gros. La plupart du temps les choses TH sont utilisées pour ne pourraient autrement être faites que si elles étaient implémentées directement comme fonctionnalités du compilateur. TH est extrêmement bénéfique d'avoir les deux parce que il vous permet de faire ces choses, et parce qu'il vous permet de prototyper des extensions potentielles de compilateur de manière beaucoup plus légère et réutilisable (voir les différents packages de lentilles, par exemple).

Pour résumer pourquoi je pense qu'il existe des sentiments négatifs à l'égard de Template Haskell: cela résout de nombreux problèmes, mais pour chaque problème qu'il résout, il semble qu'il devrait exister une solution meilleure, plus élégante et plus disciplinée, mieux adaptée à la résolution de ce problème, une solution qui ne résout pas le problème en générant automatiquement le passe-partout, mais en supprimant le besoin de avoir le passe-passe.

* Bien que j’ai souvent l’impression que CPP présente un meilleur rapport poids/puissance pour les problèmes qu’elle peut résoudre.

EDIT 23-04-14: Ce que j’essayais fréquemment de comprendre dans ce qui précède, et c’est ce que je viens de dire récemment, c’est qu’il existe une distinction importante entre abstraction et déduplication. Une abstraction correcte entraîne souvent la déduplication en tant qu'effet secondaire et la duplication est souvent le signe révélateur d'une abstraction inadéquate, mais ce n'est pas pour cela qu'elle est valable. Une abstraction correcte est ce qui rend le code correct, compréhensible et maintenable. La déduplication ne fait que la raccourcir. Template Haskell, comme les macros en général, est un outil de déduplication.

51
glaebhoerl

Je voudrais aborder quelques points soulevés par dflemstr.

Je ne trouve pas que le fait de ne pas pouvoir dactylographier TH soit aussi inquiétant. Pourquoi? Parce que même s'il y a une erreur, ce sera toujours le temps de compilation. Je ne suis pas sûr que cela renforce mon argumentation, mais cela ressemble dans l’esprit aux erreurs que vous recevez lorsque vous utilisez des modèles en C++. Je pense cependant que ces erreurs sont plus compréhensibles que celles de C++, car vous obtiendrez une jolie version imprimée du code généré.

Si une TH expression/quasi-quoteur fait quelque chose d'aussi avancé que des recoins difficiles peuvent cacher, alors c'est peut-être mal avisé?

Je ne respecte pas cette règle avec les quasi-citateurs sur lesquels j'ai travaillé récemment (en utilisant haskell-src-exts/meta) - https://github.com/mgsloan/quasi-extras/tree/master/exemples . Je sais que cela introduit certains bugs, comme l'impossibilité de se connecter à la liste générale. Cependant, je pense qu'il y a de bonnes chances que certaines des idées de http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal se retrouvent dans le compilateur. Jusque-là, les bibliothèques permettant d'analyser Haskell dans les arbres TH constituent une approximation presque parfaite.

En ce qui concerne la vitesse de compilation/les dépendances, nous pouvons utiliser le paquet "zeroth" pour intégrer le code généré. C'est au moins agréable pour les utilisateurs d'une bibliothèque donnée, mais nous ne pouvons pas faire beaucoup mieux pour l'édition de la bibliothèque. Les dépendances TH peuvent-elles faire gonfler les fichiers binaires générés? Je pensais qu'il laissait de côté tout ce qui n'était pas référencé par le code compilé.

La restriction/fractionnement des étapes de compilation du module Haskell n’a pas de sens.

RE Opacity: Il en va de même pour toutes les fonctions de bibliothèque que vous appelez. Vous n'avez aucun contrôle sur ce que fera Data.List.groupBy. Vous avez juste une "garantie"/convention raisonnable que les numéros de version vous disent quelque chose sur la compatibilité. C'est un peu une question de changement quand.

C’est là que l’utilisation de zeroth est payante - vous avez déjà configuré les fichiers générés - afin que vous sachiez toujours quand la forme du code généré a changé. L'examen des diffs peut paraître un peu compliqué, cependant, pour de grandes quantités de code généré, c'est donc un endroit où une meilleure interface de développement serait pratique.

RE Monolithisme: Vous pouvez certainement post-traiter les résultats d'une expression TH, en utilisant votre propre code de compilation. Ce ne serait pas beaucoup de code à filtrer sur le type/nom de déclaration de niveau supérieur. Heck, vous pouvez imaginer écrire une fonction qui le fait de manière générique. Pour modifier/démonolithiser les quasiquoteurs, vous pouvez associer des motifs sur "QuasiQuoter" et extraire les transformations utilisées, ou en créer une nouvelle par rapport à l'ancienne.

28
mgsloan

Cette réponse répond aux problèmes soulevés par illissius, point par point:

  • C'est moche à utiliser. $ (fooBar '' Asdf) n'a tout simplement pas l'air bien. Superficiel, certes, mais cela contribue.

Je suis d'accord. J'ai l'impression que $ () a été choisi pour paraître comme faisant partie du langage - en utilisant la palette de symboles familière de Haskell. Toutefois, c’est exactement ce que vous/ne/ne voulez pas dans les symboles utilisés pour l’épissage des macros. Ils se fondent définitivement trop, et cet aspect cosmétique est très important. J'aime l'aspect de {{}} pour les épissures, car elles sont très distinctes visuellement.

  • C'est encore plus laid d'écrire. Les citations fonctionnent parfois, mais vous devez souvent effectuer des greffes et des travaux de plomberie manuellement AST. La [API] [1] est volumineuse et difficile à manier, il y a toujours beaucoup de cas qui ne vous intéressent pas mais dont vous avez toujours besoin d'envoyer, et les cas qui vous intéressent ont tendance à se présenter sous plusieurs formes similaires mais non identiques (données). newtype, style record vs constructeurs normaux, etc.). C'est assez ennuyeux et répétitif d'écrire et assez compliqué pour ne pas être mécanique. La [proposition de réforme] [2] en traite en partie (en rendant les citations plus largement applicables).

Je suis également d'accord avec cela, cependant, comme le notent certains des commentaires de "Nouvelles orientations pour TH", le manque de bonnes citations prêtes à l'emploi AST n'est pas un défaut critique. Dans ce paquet WIP, je cherche à résoudre ces problèmes sous forme de bibliothèque: https://github.com/mgsloan/quasi-extras . Jusqu'à présent, j'autorise l'épissage dans un peu plus d'endroits que d'habitude et je peux reproduire les motifs sur les AST.

  • La restriction de l'étape est l'enfer. Ne pas pouvoir épisser les fonctions définies dans le même module en est la partie la plus petite: l'autre conséquence est que si vous avez une épissure de niveau supérieur, tout ce qui le suit dans le module sera hors de portée par rapport à ce qu'il était avant. D'autres langages avec cette propriété (C, C++) le rendent utilisable en vous permettant de transmettre des déclarations, mais pas Haskell. Si vous avez besoin de références cycliques entre des déclarations épissées ou leurs dépendances et leurs dépendants, vous êtes généralement simplement foutu.

Je suis tombé sur le problème des définitions cycliques TH impossibles auparavant ... C'est assez énervant. Il existe une solution, mais elle est moche - englobez les éléments impliqués dans la dépendance cyclique dans une expression TH combinant toutes les déclarations générées. L'un des générateurs de ces déclarations pourrait simplement être un quasi-quotateur qui accepte le code Haskell.

  • C'est sans principes. Ce que je veux dire par là, c'est que la plupart du temps, lorsque vous exprimez une abstraction, il existe une sorte de principe ou de concept derrière cette abstraction. Pour beaucoup d'abstractions, le principe qui les sous-tend peut être exprimé dans leurs types. Lorsque vous définissez une classe de type, vous pouvez souvent formuler des lois auxquelles les instances doivent obéir et que les clients peuvent assumer. Si vous utilisez la [nouvelle fonctionnalité générique] [3] de GHC pour résumer la forme d'une déclaration d'instance sur tout type de données (dans les limites), vous obtenez "pour les types sum, cela fonctionne comme ceci, pour les types de produit, cela fonctionne comme ça ". Mais Template Haskell n'est que des macros stupides. Ce n'est pas une abstraction au niveau des idées, mais une abstraction au niveau des AST, ce qui est mieux, mais modestement, que l'abstraction au niveau du texte brut.

C'est seulement sans principes si vous faites des choses sans principes avec. La seule différence est qu'avec les mécanismes d'abstraction mis en œuvre par le compilateur, vous avez plus de confiance que l'abstraction n'est pas perméable. Peut-être que la conception du langage en cours de démocratisation semble un peu effrayante! Les créateurs de bibliothèques TH doivent bien documenter et définir clairement le sens et les résultats des outils fournis. Un bon exemple de principe TH est le package de dérivation: http://hackage.haskell.org/package/derive - il utilise un DSL tel que l'exemple de beaucoup de dérivations/spécifie/la dérivation réelle.

  • Cela vous lie à GHC. En théorie, un autre compilateur pourrait l'implémenter, mais en pratique, je doute que cela se produise un jour. (Cela contraste avec diverses extensions de systèmes de types qui, même si elles ne sont actuellement mises en œuvre que par GHC, sont faciles à imaginer en train d’être adoptées par d’autres compilateurs et finalement normalisées.)

C'est un très bon point - l'API TH est assez grande et maladroite. Réappliquer cela semble être une tâche difficile. Cependant, il n’ya vraiment que quelques façons de résoudre le problème de la représentation des AST de Haskell. J'imagine que copier les ADT TH et écrire un convertisseur sur la représentation interne AST vous aiderait beaucoup. Cela équivaudrait à l’effort (non négligeable) de création de haskell-src-meta. Il pourrait également être simplement ré-implémenté en imprimant assez le TH AST et en utilisant l'analyseur interne du compilateur.

Bien que je puisse me tromper, je ne vois pas TH comme étant aussi compliqué d'une extension de compilateur, du point de vue de la mise en œuvre. C’est en fait l’un des avantages de "garder les choses simples" et de ne pas avoir la couche fondamentale comme système de gabarit attrayant du point de vue théorique, statistiquement vérifiable.

  • L'API n'est pas stable. Lorsque de nouvelles fonctionnalités linguistiques sont ajoutées à GHC et que le package template-haskell est mis à jour pour les prendre en charge, des modifications incompatibles avec les versions antérieures des types de données TH sont incompatibles avec les versions antérieures. Si vous voulez que votre code TH soit compatible avec plusieurs versions de GHC, vous devez être très prudent et utiliser éventuellement CPP.

C'est aussi un bon point, mais quelque peu dramatique. Bien qu’il y ait eu des ajouts d’API récemment, ils n’ont pas été générateurs de bris. De plus, je pense qu'avec la citation supérieure AST que j'ai mentionnée plus tôt, l'IPA qui doit réellement être utilisé peut être considérablement réduit. Si aucune construction/correspondance n'a besoin de fonctions distinctes et n'est exprimée que sous forme de littéraux, la plupart des API disparaissent. De plus, le code que vous écrivez porterait plus facilement vers des représentations AST pour des langages similaires à Haskell.


En résumé, je pense que TH est un puissant outil semi-négligé. Moins de haine pourrait conduire à un éco-système de bibliothèques plus vivant, encourageant la mise en œuvre de plus de prototypes d’entités linguistiques. Il a été observé que TH est un outil surpuissant, qui peut vous laisser/faire/presque n'importe quoi. Anarchie! Eh bien, j’estime que ce pouvoir peut vous permettre de surmonter la plupart de ses limites et de construire des systèmes capables d’appliquer des approches de méta-programmation assez fondées. Il vaut la peine d'utiliser des hacks laids pour simuler la mise en œuvre "correcte", car ainsi la conception de la mise en oeuvre "appropriée" deviendra progressivement claire.

Dans ma version idéale personnelle du nirvana, une grande partie du langage serait en fait transférée hors du compilateur, dans des bibliothèques de cette variété. Le fait que les fonctionnalités soient implémentées en tant que bibliothèques n'influence pas fortement leur capacité à analyser fidèlement.

Quelle est la réponse typique de Haskell au code standard? Abstraction. Quelles sont nos abstractions préférées? Fonctions et classes de types!

Les classes de types nous permettent de définir un ensemble de méthodes, qui peuvent ensuite être utilisées dans toutes sortes de fonctions génériques sur cette classe. Cependant, mis à part cela, les classes ne peuvent contribuer à éviter les problèmes habituels qu'en offrant des "définitions par défaut". Voici maintenant un exemple de fonctionnalité sans principe!

  • Les ensembles de liaisons minimales ne sont pas déclarables/vérifiables par le compilateur. Cela pourrait conduire à des définitions inattendues générant des pertes en raison d'une récursion mutuelle.

  • En dépit de la grande commodité et de la puissance qui en résulteraient, vous ne pouvez pas spécifier les valeurs par défaut de la super-classe, en raison des instances orphelines http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ = Cela nous permettrait de régler la hiérarchie numérique avec élégance!

  • Aller au bout des capacités de type TH pour les méthodes par défaut a conduit à http://www.haskell.org/haskellwiki/GHC.Generics . Bien que ce soit quelque chose de sympa, ma seule expérience de débogage de code avec ces génériques était quasiment impossible, en raison de la taille du type induit pour ADT et aussi compliqué qu’un AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    En d’autres termes, cela s’appliquait aux fonctionnalités fournies par TH, mais il fallait que tout un domaine du langage, le langage de construction, soit transformé en système de types. Bien que je puisse voir que cela fonctionne bien pour votre problème commun, pour les problèmes complexes, il semble enclin à donner une pile de symboles beaucoup plus terrifiante que TH le piratage informatique.

    TH vous donne un calcul du code de sortie au moment de la compilation au niveau de la valeur, alors que les génériques vous obligent à lever la partie correspondance du modèle/récursivité du code dans le système de types. Bien que cela limite l'utilisateur de quelques manières assez utiles, je ne pense pas que la complexité en vaille la peine.

Je pense que le rejet de la métaprogrammation TH et de type LISP a conduit à privilégier des éléments tels que les méthodes par défaut plutôt que des méthodes plus souples, une macro-expansion similaire à celle des déclarations d'instances. Il est judicieux d'éviter les choses qui pourraient conduire à des résultats imprévus. Cependant, nous ne devons pas ignorer que le système de types performant de Haskell permet une métaprogrammation plus fiable que dans de nombreux autres environnements (en vérifiant le code généré).

14
mgsloan

Un problème plutôt pragmatique avec Template Haskell est que cela ne fonctionne que lorsque l'interpréteur de bytecode de GHC est disponible, ce qui n'est pas le cas sur toutes les architectures. Ainsi, si votre programme utilise Template Haskell ou s'appuie sur des bibliothèques qui l'utilisent, il ne fonctionnera pas sur des machines dotées d'un processeur ARM, MIPS, S390 ou PowerPC.

Ceci est pertinent dans la pratique: git-annex est un outil écrit en Haskell qui a du sens pour fonctionner sur des machines soucieuses de stockage, ces machines ont souvent des processeurs non i386. Personnellement, je lance git-annex sur un NSLU 2 (32 Mo de RAM, 266 MHz; saviez-vous que Haskell fonctionnait correctement avec un tel matériel?) S'il utilisait Template Haskell, ce n'est pas possible.

(La situation à propos de GHC sur ARM s'améliore beaucoup ces jours-ci et je pense que 7.4.2 fonctionne même, mais le problème persiste).

8
Joachim Breitner

Pourquoi est TH mauvais? Pour moi, cela revient à ceci:

Si vous devez produire autant de code répétitif que vous essayez d’utiliser TH pour le générer automatiquement, vous le faites mal!

Penses-y. Haskell est séduit par le fait que sa conception de haut niveau vous permet d'éviter d'énormes quantités de code passe-partout inutiles que vous devez écrire dans d'autres langues. Si vous besoin génération de code au moment de la compilation, vous dites en gros que votre langage ou la conception de votre application vous ont échoué. Et nous, les programmeurs, n'aimons pas échouer.

Parfois, bien sûr, c'est nécessaire. Mais parfois, vous pouvez éviter d’avoir besoin de TH) simplement en étant un peu plus intelligent avec vos conceptions.

(L'autre chose est que TH est un niveau assez bas. Il n'y a pas de conception de haut niveau, mais de nombreux détails de l'implémentation interne de GHC sont exposés. Et cela rend l'API susceptible de changer. .)

6
MathematicalOrchid