Il semble que Template Haskell soit souvent considéré par la communauté Haskell comme une commodité malheureuse. Il est difficile de mettre en mots exactement ce que j'ai observé à cet égard, mais considérez ces quelques exemples
- Modèle Haskell répertorié sous "The Ugly (but necessary)" en réponse à la question Quelles extensions Haskell (GHC) les utilisateurs devraient-ils utiliser / éviter?
- Modèle Haskell considéré comme une solution temporaire / inférieure dans les vecteurs non emballés de thread de valeurs de type nouveau (liste de diffusion des bibliothèques)
- Yesod est souvent critiqué pour trop s'appuyer sur Template Haskell (voir le blog en réponse à ce sentiment)
J'ai vu divers articles de blog où les gens font des trucs assez soignés avec Template Haskell, permettant une syntaxe plus jolie qui ne serait tout simplement pas possible dans Haskell normal, ainsi qu'une réduction considérable du passe-partout. Alors pourquoi est-ce que Template Haskell est méprisé de cette façon? Qu'est-ce qui le rend indésirable? Dans quelles circonstances devrait-on éviter Template Haskell, et pourquoi?
haskell
template-haskell
Dan Burton
la source
la source
Réponses:
Une des raisons pour éviter le Template Haskell est qu'il n'est pas du tout protégé par type, ce qui va à l'encontre de "l'esprit de Haskell". En voici quelques exemples:
Exp
, mais vous ne savez pas si c'est une expression qui représente un[Char]
ou un(a -> (forall b . b -> c))
ou quoi que ce soit. TH serait plus fiable si l'on pouvait exprimer qu'une fonction ne peut générer que des expressions d'un certain type, ou seulement des déclarations de fonction, ou seulement des modèles de correspondance de constructeur de données, etc.foo
qui n'existe pas? Heureusement, 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 de faire des tests unitaires.TH est également carrément dangereux:
IO
, y compris le lancement de missiles ou le vol de votre carte de crédit. Vous ne voulez pas avoir à parcourir chaque paquet cabale que vous avez téléchargé à la recherche d'exploits TH.Ensuite, il y a quelques problèmes qui rendent les fonctions TH moins amusantes à utiliser en tant que développeur de bibliothèque:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
est juste une monade, vous pouvez donc utiliser toutes les fonctions habituelles dessus. Certaines personnes ne le savent pas, et à cause de cela, elles créent plusieurs versions surchargées essentiellement des mêmes fonctions avec les mêmes fonctionnalités, et ces fonctions conduisent à un certain effet de ballonnement. De plus, la plupart des gens écrivent leurs générateurs dans laQ
monade même lorsqu'ils n'y sont pas obligés, ce qui revient à écrirebla :: IO Int; bla = return 3
; vous donnez à une fonction plus d '«environnement» qu'elle n'en a besoin, et les clients de la fonction sont tenus de fournir cet environnement en conséquence.Enfin, il y a certaines choses qui rendent les fonctions TH moins amusantes à utiliser en tant qu'utilisateur final:
Q Dec
, elle peut générer absolument n'importe quoi au niveau supérieur d'un module, et vous n'avez absolument aucun contrôle sur ce qui sera généré.la source
Ce n'est que ma propre opinion.
C'est moche à utiliser.
$(fooBar ''Asdf)
n'a tout simplement pas l'air sympa. Superficiel, bien sûr, mais ça y contribue.C'est encore plus laid d'écrire. Citer fonctionne parfois, mais la plupart du temps, vous devez effectuer une greffe et une plomberie AST manuelles. L' API est grande et peu maniable, il y a toujours beaucoup de cas dont vous ne vous souciez pas mais que vous devez toujours envoyer, et les cas dont vous vous souciez ont tendance à être présents sous plusieurs formes similaires mais pas identiques (données vs nouveau type, enregistrement -style vs constructeurs normaux, etc.). C'est ennuyeux et répétitif d'écrire et assez compliqué pour ne pas être mécanique. La proposition de réforme aborde certains de ces points (rendant les citations plus largement applicables).
La restriction de scène est l'enfer. Ne pas pouvoir épisser des fonctions définies dans le même module est la plus petite partie de celui-ci: l'autre conséquence est que si vous avez une épissure de niveau supérieur, tout ce qui se trouve après dans le module sera hors de portée de tout ce qui le précède. D'autres langages avec cette propriété (C, C ++) la rendent réalisable en vous permettant de transmettre des choses, mais pas Haskell. Si vous avez besoin de références cycliques entre des déclarations épissées ou leurs dépendances et dépendances, vous êtes généralement juste foutu.
C'est indiscipliné. Ce que je veux dire par là, c'est que la plupart du temps, lorsque vous exprimez une abstraction, il y a une sorte de principe ou de concept derrière cette abstraction. Pour de nombreuses abstractions, le principe sous-jacent peut être exprimé dans leurs types. Pour les classes 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 de GHC pour abstraire la forme d'une déclaration d'instance sur n'importe quel type de données (dans des limites), vous pouvez dire "pour les types de somme, cela fonctionne comme ceci, pour les types de produits, cela fonctionne comme ça". Le modèle Haskell, en revanche, n'est que des macros. Ce n'est pas l'abstraction au niveau des idées, mais l'abstraction au niveau des AST, ce qui est mieux, mais modestement, que l'abstraction au niveau du texte brut. *
Cela vous lie au GHC. En théorie, un autre compilateur pourrait l'implémenter, mais dans la pratique, je doute que cela se produise. (Cela contraste avec les différentes extensions de système de type qui, bien qu'elles ne puissent être implémentées que par GHC pour le moment, je pourrais facilement imaginer qu'elles soient adoptées par d'autres compilateurs plus tard et finalement normalisées.)
L'API n'est pas stable. Lorsque de nouvelles fonctionnalités de langage sont ajoutées à GHC et que le package template-haskell est mis à jour pour les prendre en charge, cela implique souvent des modifications incompatibles en amont des types de données TH. Si vous voulez que votre code TH soit compatible avec plus d'une seule version de GHC, vous devez être très prudent et éventuellement utiliser
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, le modèle Haskell est quelque chose comme ça . S'il y a un moyen de le faire qui n'est pas Template Haskell, c'est généralement préférable.
L'avantage de Template Haskell est que vous pouvez faire avec lui des choses que vous ne pourriez pas faire autrement, et c'est un gros problème. La plupart du temps, les choses pour lesquelles TH est utilisé ne pourraient autrement être faites que si elles étaient implémentées directement en tant que fonctionnalités du compilateur. TH est extrêmement avantageux d'avoir à la fois parce qu'il vous permet de faire ces choses et parce qu'il vous permet de prototyper les extensions potentielles du compilateur d'une 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 y a des sentiments négatifs envers Template Haskell: il résout beaucoup de problèmes, mais pour tout problème donné qu'il résout, il semble qu'il devrait y avoir une meilleure solution, plus élégante et disciplinée, mieux adaptée pour résoudre ce problème, celui qui ne résout pas le problème en générant automatiquement le passe-partout, mais en supprimant la nécessité d' avoir le passe-partout.
* Bien que je pense souvent que le
CPP
rapport poids / puissance est meilleur pour les problèmes qu'il peut résoudre.EDIT 23-04-14: Ce que j'essayais fréquemment de faire dans ce qui précède, et que je n'ai obtenu que récemment, c'est qu'il y a une distinction importante entre abstraction et déduplication. Une abstraction correcte entraîne souvent la déduplication comme effet secondaire, et la duplication est souvent un signe révélateur d'une abstraction inadéquate, mais ce n'est pas pourquoi 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. Le modèle Haskell, comme les macros en général, est un outil de déduplication.
la source
Je voudrais aborder quelques-uns des points soulevés par dflemstr.
Je ne trouve pas le fait que vous ne puissiez pas vérifier le TH comme si inquiétant. Pourquoi? Parce que même s'il y a une erreur, ce sera toujours le temps de compilation. Je ne sais pas si cela renforce mon argument, mais cela est similaire dans l'esprit aux erreurs que vous recevez lors de l'utilisation de modèles en C ++. Je pense que ces erreurs sont plus compréhensibles que les erreurs de C ++, car vous obtiendrez une version assez imprimée du code généré.
Si une expression TH / un quasi-guillemet fait quelque chose de si avancé que des coins difficiles peuvent se cacher, alors peut-être est-il mal avisé?
Je brise un peu cette règle avec les quasi-guillemets sur lesquels j'ai travaillé récemment (en utilisant haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Je sais que cela introduit certains bugs comme le fait de ne pas pouvoir épisser dans les listes de compréhension généralisées. Cependant, je pense qu'il y a de fortes 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 pour analyser Haskell en arbres TH sont une approximation presque parfaite.
En ce qui concerne la vitesse / les dépendances de compilation, nous pouvons utiliser le package "zeroth" pour incorporer 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 mieux pour le cas de l'édition de la bibliothèque. Les dépendances TH peuvent-elles gonfler les binaires générés? Je pensais que cela laissait de côté tout ce qui n'était pas référencé par le code compilé.
La restriction de mise en scène / fractionnement des étapes de compilation du module Haskell est nul.
RE Opacity: C'est la même chose pour toute fonction de bibliothèque que vous appelez. Vous n'avez aucun contrôle sur ce que Data.List.groupBy fera. 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 différente quand.
C'est là que l'utilisation de zeroth est payante - vous êtes déjà en train de versionner les fichiers générés - donc vous saurez toujours quand la forme du code généré a changé. En regardant les différences, cela peut être un peu noueux, cependant, pour de grandes quantités de code généré, c'est donc un endroit où une meilleure interface de développeur serait pratique.
Monolithisme RE: 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 pourriez imaginer écrire une fonction qui le fait de manière générique. Pour modifier / démonolithiser les quasiquoteurs, vous pouvez faire correspondre les motifs sur "QuasiQuoter" et extraire les transformations utilisées, ou en créer une nouvelle en fonction de l'ancienne.
la source
[Dec]
et supprimer des éléments que vous ne voulez pas, mais disons que la fonction lit un fichier de définition externe lors de la génération de l'interface JSON. Ce n'est pas parce que vous n'utilisez pas queDec
le générateur arrête de chercher le fichier de définition, ce qui fait échouer la compilation. Pour cette raison, ce serait bien d'avoir une version plus restrictive de laQ
monade qui vous permettrait de générer de nouveaux noms (et de telles choses) mais pas de permettreIO
, de sorte que, comme vous le dites, on pourrait filtrer ses résultats / faire d'autres compositions .Cette réponse répond aux questions soulevées par illissius, point par point:
Je suis d'accord. J'ai l'impression que $ () a été choisi pour ressembler à une partie du langage - en utilisant la palette de symboles familière de Haskell. Cependant, c'est exactement ce que vous / ne voulez / pas dans les symboles utilisés pour votre épissage de macro. Ils se fondent définitivement trop et cet aspect cosmétique est assez important. J'aime l'apparence de {{}} pour les épissures, car elles sont assez distinctes visuellement.
Je suis également d'accord avec cela, cependant, comme certains des commentaires dans "New Directions for TH" l'observent, le manque de bonnes citations AST prêtes à l'emploi n'est pas un défaut critique. Dans ce package 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 à quelques endroits de plus que d'habitude et je peux faire correspondre les motifs sur les AST.
J'ai rencontré le problème des définitions cycliques de TH qui étaient impossibles auparavant ... C'est assez ennuyeux. Il y a une solution, mais c'est moche - envelopper les choses impliquées dans la dépendance cyclique dans une expression TH qui combine toutes les déclarations générées. L'un de ces générateurs de déclarations pourrait simplement être un quasi-guillemet qui accepte le code Haskell.
Ce n'est sans principe que si vous faites des choses sans principe 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 étanche. Peut-être que la démocratisation de la conception du langage semble un peu effrayante! Les créateurs de bibliothèques TH doivent bien documenter et définir clairement la signification et les résultats des outils qu'ils fournissent. Un bon exemple de TH basé sur des principes est le paquet dérivé: http://hackage.haskell.org/package/derive - il utilise un DSL tel que l'exemple de nombreuses dérivations / spécifie / la dérivation réelle.
C'est un très bon point - l'API TH est assez grande et maladroite. La réimplémentation semble être difficile. Cependant, il n'y a vraiment que quelques façons de résoudre le problème de la représentation des AST Haskell. J'imagine que la copie des TH ADT et l'écriture d'un convertisseur vers la représentation AST interne vous aideraient 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 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é qu'une extension de compilateur, du point de vue de l'implémentation. C'est en fait l'un des avantages de "garder les choses simples" et de ne pas avoir la couche fondamentale d'un système de modèle théoriquement attrayant et statiquement vérifiable.
C'est également un bon point, mais quelque peu dramatisé. Bien qu'il y ait eu des ajouts d'API récemment, ils n'ont pas induit de ruptures importantes. De plus, je pense qu'avec la citation AST supérieure que j'ai mentionnée plus tôt, l'API qui doit réellement être utilisée peut être très considérablement réduite. Si aucune construction / correspondance n'a besoin de fonctions distinctes et qu'elles sont plutôt exprimées en littéraux, la plupart de l'API disparaît. De plus, le code que vous écrivez porterait plus facilement vers les représentations AST pour les langages similaires à Haskell.
En résumé, je pense que TH est un outil puissant et semi-négligé. Moins de haine pourrait conduire à un écosystème de bibliothèques plus vivant, encourageant la mise en œuvre de prototypes de fonctionnalités plus linguistiques. Il a été observé que TH est un outil surpuissant, qui peut vous permettre / faire / presque n'importe quoi. Anarchie! Eh bien, je pense que ce pouvoir peut vous permettre de surmonter la plupart de ses limites et de construire des systèmes capables d'approches de méta-programmation assez fondées sur des principes. Cela vaut la peine d'utiliser de vilains hacks pour simuler l'implémentation "correcte", car de cette façon, la conception de l'implémentation "correcte" deviendra progressivement claire.
Dans ma version idéale personnelle de nirvana, une grande partie du langage se déplacerait en fait 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é à faire un résumé fidèle.
Quelle est la réponse typique de Haskell au code passe-partout? Abstraction. Quelles sont nos abstractions préférées? Fonctions et typo!
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, à part cela, la seule façon pour les classes d'éviter le passe-partout est de proposer des "définitions par défaut". Maintenant, voici un exemple d'une fonctionnalité sans principes!
Les ensembles de liaisons minimales ne sont pas déclarables / vérifiables par le compilateur. Cela pourrait conduire à des définitions par inadvertance qui donnent le bas en raison de la récurrence mutuelle.
Malgré la grande commodité et la puissance que cela produirait, vous ne pouvez pas spécifier les paramètres par défaut de la superclasse, en raison d'instances orphelines http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Ceux-ci nous permettraient de corriger le hiérarchie numérique gracieusement!
La recherche de capacités de type TH pour les valeurs par défaut des méthodes a conduit à http://www.haskell.org/haskellwiki/GHC.Generics . Bien que ce soit cool, ma seule expérience de débogage de code en utilisant ces génériques était presque impossible, en raison de la taille du type induit pour et de l'ADT aussi compliqué qu'un AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
En d'autres termes, cela allait après les fonctionnalités fournies par TH, mais cela devait soulever un domaine entier du langage, le langage de construction, dans une représentation de système de types. Bien que je puisse le voir bien fonctionner pour votre problème commun, pour les problèmes complexes, il semble enclin à produire une pile de symboles beaucoup plus terrifiant que le piratage TH.
TH vous donne le calcul au niveau de la valeur au moment de la compilation du code de sortie, tandis que les génériques vous obligent à soulever la partie de correspondance de motif / 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 TH et de la métaprogrammation de type lisp a conduit à la préférence pour des choses comme les méthodes par défaut plutôt que pour des déclarations d'instances plus flexibles, comme la macro-expansion. La discipline d'éviter les choses qui pourraient conduire à des résultats imprévus est sage, cependant, nous ne devons pas ignorer que le système de type capable de Haskell permet une métaprogrammation plus fiable que dans de nombreux autres environnements (en vérifiant le code généré).
la source
Un problème assez pragmatique avec Template Haskell est qu'il ne fonctionne que lorsque l'interpréteur de bytecode de GHC est disponible, ce qui n'est pas le cas sur toutes les architectures. Donc, si votre programme utilise Template Haskell ou s'appuie sur des bibliothèques qui l'utilisent, il ne fonctionnera pas sur les machines avec 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, CPU 266 MHz; saviez-vous que Haskell fonctionne bien sur un tel matériel?) S'il utiliserait le modèle Haskell, ce n'est pas possible.
(La situation concernant GHC sur ARM s'améliore beaucoup ces jours-ci et je pense que 7.4.2 fonctionne même, mais le point est toujours valable).
la source
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)Pourquoi TH est-il mauvais? Pour moi, cela se résume à ceci:
Pensez-y. La moitié de l'attrait de Haskell est que sa conception de haut niveau vous permet d'éviter d'énormes quantités de code passe-partout inutile que vous devez écrire dans d'autres langues. Si vous avez besoin de génération de code au moment de la compilation, vous dites essentiellement que votre langue ou la conception de votre application vous a é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 en étant un peu plus intelligent avec vos conceptions.
(L'autre chose est que TH est assez bas niveau. Il n'y a pas de grande conception de haut niveau; beaucoup de détails d'implémentation internes de GHC sont exposés. Et cela rend l'API sujette à changement ...)
la source