Es scheint, dass Template Haskell von der Haskell-Community oft als unglückliche Annehmlichkeit angesehen wird. Es ist schwer, genau in Worte zu fassen, was ich in dieser Hinsicht beobachtet habe, aber betrachten Sie diese wenigen Beispiele
- Vorlage Haskell unter "The Ugly (aber notwendig)" als Antwort auf die Frage Welche Haskell (GHC) -Erweiterungen sollten Benutzer verwenden / vermeiden?
- Vorlage Haskell betrachtete eine temporäre / minderwertige Lösung in Unboxed Vectors of Newtype'd Values Thread (Bibliotheken Mailingliste)
- Jessod wird oft dafür kritisiert, dass er sich zu sehr auf Template Haskell verlässt (siehe den Blog-Beitrag als Antwort auf dieses Gefühl).
Ich habe verschiedene Blog-Posts gesehen, in denen Leute mit Template Haskell ziemlich nette Sachen machen, die eine schönere Syntax ermöglichen, die in normalem Haskell einfach nicht möglich wäre, sowie eine enorme Reduzierung der Boilerplate. Warum wird Template Haskell auf diese Weise herabgesehen? Was macht es unerwünscht? Unter welchen Umständen sollte Template Haskell vermieden werden und warum?
haskell
template-haskell
Dan Burton
quelle
quelle
Antworten:
Ein Grund für die Vermeidung von Template Haskell ist, dass es als Ganzes überhaupt nicht typsicher ist und somit gegen einen Großteil des "Geistes von Haskell" verstößt. Hier einige Beispiele dafür:
Exp
, wissen aber nicht, ob es sich um einen Ausdruck handelt, der ein[Char]
oder ein(a -> (forall b . b -> c))
oder was auch immer darstellt. TH wäre zuverlässiger, wenn man ausdrücken könnte, dass eine Funktion nur Ausdrücke eines bestimmten Typs oder nur Funktionsdeklarationen oder nur Datenkonstruktor-Übereinstimmungsmuster usw. erzeugen kann.foo
verweist, die nicht vorhanden ist? Pech, Sie werden das nur sehen, wenn Sie Ihren Codegenerator tatsächlich verwenden, und nur unter den Umständen, die die Generierung dieses bestimmten Codes auslösen. Es ist auch sehr schwierig, einen Unit-Test durchzuführen.TH ist auch geradezu gefährlich:
IO
, einschließlich des Abschusses von Raketen oder des Diebstahls Ihrer Kreditkarte. Sie möchten nicht jedes Kabalenpaket durchsuchen müssen, das Sie jemals heruntergeladen haben, um nach TH-Exploits zu suchen.Dann gibt es einige Probleme, die die Verwendung von TH-Funktionen als Bibliotheksentwickler weniger unterhaltsam machen:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
ist nur eine Monade, so dass Sie alle üblichen Funktionen darauf verwenden können. Einige Leute wissen das nicht und erstellen aus diesem Grund mehrere überladene Versionen von im Wesentlichen denselben Funktionen mit derselben Funktionalität, und diese Funktionen führen zu einem gewissen Aufblähungseffekt. Außerdem schreiben die meisten Leute ihre Generatoren in dieQ
Monade, auch wenn sie es nicht müssen, was wie Schreiben istbla :: IO Int; bla = return 3
; Sie geben einer Funktion mehr "Umgebung" als sie benötigt, und Clients der Funktion müssen diese Umgebung als Folge davon bereitstellen.Schließlich gibt es einige Dinge, die die Verwendung von TH-Funktionen als Endbenutzer weniger unterhaltsam machen:
Q Dec
, kann sie auf der obersten Ebene eines Moduls absolut alles generieren, und Sie haben absolut keine Kontrolle darüber, was generiert wird.quelle
Dies ist ausschließlich meine eigene Meinung.
Es ist hässlich zu benutzen.
$(fooBar ''Asdf)
sieht einfach nicht gut aus. Oberflächlich, sicher, aber es trägt dazu bei.Es ist noch hässlicher zu schreiben. Das Zitieren funktioniert manchmal, aber die meiste Zeit müssen Sie manuell AST-Transplantationen und -Installationen durchführen. Das API ist groß und unhandlich, es gibt immer viele Fälle, die Sie nicht interessieren, aber dennoch versenden müssen, und die Fälle, die Sie interessieren, sind in der Regel in mehreren ähnlichen, aber nicht identischen Formen vorhanden (Daten vs. Newtype, Datensatz) -Stil gegen normale Konstruktoren und so weiter). Es ist langweilig und sich wiederholend zu schreiben und kompliziert genug, um nicht mechanisch zu sein. Der Reformvorschlag befasst sich mit einigen dieser Fragen (wodurch Zitate allgemeiner anwendbar werden).
Die Bühnenbeschränkung ist die Hölle. Die Möglichkeit, Funktionen, die im selben Modul definiert sind, nicht zu verbinden, ist der kleinere Teil davon. Die andere Konsequenz ist, dass, wenn Sie einen Spleiß der obersten Ebene haben, alles, was danach im Modul vorhanden ist, für alles, was davor liegt, außer Reichweite ist. Andere Sprachen mit dieser Eigenschaft (C, C ++) machen es funktionsfähig, indem Sie deklarierte Dinge weiterleiten können, Haskell jedoch nicht. Wenn Sie zyklische Verweise zwischen gespleißten Deklarationen oder deren Abhängigkeiten und Abhängigkeiten benötigen, werden Sie normalerweise nur geschraubt.
Es ist undiszipliniert. Damit meine ich, dass die meiste Zeit, wenn Sie eine Abstraktion ausdrücken, hinter dieser Abstraktion ein Prinzip oder ein Konzept steckt. Für viele Abstraktionen kann das Prinzip dahinter in ihren Typen ausgedrückt werden. Für Typklassen können Sie häufig Gesetze formulieren, denen Instanzen gehorchen sollten und die Clients annehmen können. Wenn Sie die neue Generika-Funktion von GHC verwenden von um die Form einer Instanzdeklaration über einen beliebigen Datentyp (innerhalb von Grenzen) zu abstrahieren, können Sie sagen: "Für Summentypen funktioniert das so, für Produkttypen funktioniert es so". Template Haskell hingegen ist nur ein Makro. Es ist keine Abstraktion auf der Ebene der Ideen, sondern eine Abstraktion auf der Ebene der ASTs, die besser, aber nur bescheiden ist als die Abstraktion auf der Ebene des Klartextes. *
Es bindet Sie an GHC. Theoretisch könnte es ein anderer Compiler implementieren, aber in der Praxis bezweifle ich, dass dies jemals passieren wird. (Dies steht im Gegensatz zu verschiedenen Typ-Systemerweiterungen, die, obwohl sie derzeit möglicherweise nur von GHC implementiert werden, leicht vorstellbar sind, später von anderen Compilern übernommen und schließlich standardisiert zu werden.)
Die API ist nicht stabil. Wenn GHC neue Sprachfunktionen hinzugefügt und das Template-Haskell-Paket aktualisiert wird, um diese zu unterstützen, sind häufig abwärtsinkompatible Änderungen an den TH-Datentypen erforderlich. Wenn Sie möchten, dass Ihr TH-Code mit mehr als nur einer Version von GHC kompatibel ist, müssen Sie sehr vorsichtig sein und möglicherweise verwenden
CPP
.Es gibt ein allgemeines Prinzip, dass Sie das richtige Werkzeug für den Job verwenden sollten und das kleinste, das ausreicht, und in dieser Analogie ist Vorlage Haskell so etwas . Wenn es eine Möglichkeit gibt, die nicht Template Haskell ist, ist dies im Allgemeinen vorzuziehen.
Der Vorteil von Template Haskell ist, dass Sie damit Dinge tun können, die Sie auf keine andere Weise tun könnten, und es ist eine große Sache. Meistens könnten die Dinge, für die TH verwendet wird, sonst nur ausgeführt werden, wenn sie direkt als Compiler-Funktionen implementiert würden. TH ist äußerst vorteilhaft, da Sie damit diese Aufgaben ausführen und potenzielle Compiler-Erweiterungen viel leichter und wiederverwendbarer prototypisieren können (siehe z. B. die verschiedenen Objektivpakete).
Um zusammenzufassen, warum ich denke, dass es negative Gefühle gegenüber Template Haskell gibt: Es löst viele Probleme, aber für jedes gegebene Problem, das es löst, scheint es eine bessere, elegantere, diszipliniertere Lösung zu geben, die besser zur Lösung dieses Problems geeignet ist. eine , die das Problem nicht , indem sie automatisch die vorformulierten Erzeugung, sondern durch die Beseitigung der Notwendigkeit löst haben die vorformulierten.
* Obwohl ich oft das Gefühl habe, dass
CPP
das Leistungsgewicht für die Probleme, die es lösen kann, besser ist.EDIT 23-04-14: Was ich oben häufig versucht habe und erst kürzlich genau erreicht habe, ist, dass es einen wichtigen Unterschied zwischen Abstraktion und Deduplizierung gibt. Die richtige Abstraktion führt häufig zu einer Deduplizierung als Nebeneffekt, und die Duplizierung ist oft ein verräterisches Zeichen für eine unzureichende Abstraktion, aber das ist nicht der Grund, warum sie wertvoll ist. Die richtige Abstraktion macht Code korrekt, verständlich und wartbar. Die Deduplizierung macht es nur kürzer. Template Haskell ist wie Makros im Allgemeinen ein Tool zur Deduplizierung.
quelle
Ich möchte einige der Punkte ansprechen, die dflemstr anspricht.
Ich finde die Tatsache, dass Sie TH nicht tippen können, nicht so besorgniserregend. Warum? Denn selbst wenn ein Fehler auftritt, bleibt die Kompilierungszeit. Ich bin nicht sicher, ob dies meine Argumentation stärkt, aber dies ähnelt im Geiste den Fehlern, die Sie bei der Verwendung von Vorlagen in C ++ erhalten. Ich denke, diese Fehler sind verständlicher als die Fehler von C ++, da Sie eine hübsche gedruckte Version des generierten Codes erhalten.
Wenn ein TH-Ausdruck / Quasi-Quoter etwas tut, das so weit fortgeschritten ist, dass sich schwierige Ecken verbergen können, ist es vielleicht schlecht beraten?
Ich verstoße ziemlich oft gegen diese Regel mit Quasi-Quotern, an denen ich in letzter Zeit gearbeitet habe (mit haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Ich weiß, dass dies einige Fehler mit sich bringt, z. B., dass das verallgemeinerte Listenverständnis nicht gespleißt werden kann. Ich denke jedoch, dass es eine gute Chance gibt, dass einige der Ideen in http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal im Compiler landen. Bis dahin sind die Bibliotheken zum Parsen von Haskell auf TH-Bäume eine nahezu perfekte Annäherung.
In Bezug auf die Kompilierungsgeschwindigkeit / -abhängigkeiten können wir das "nullte" Paket verwenden, um den generierten Code zu integrieren. Dies ist zumindest für die Benutzer einer bestimmten Bibliothek von Vorteil, aber wir können es beim Bearbeiten der Bibliothek nicht viel besser machen. Können TH-Abhängigkeiten generierte Binärdateien aufblähen? Ich dachte, es würde alles ausgelassen, worauf der kompilierte Code nicht verweist.
Die Staging-Einschränkung / Aufteilung der Kompilierungsschritte des Haskell-Moduls ist zum Kotzen.
RE-Deckkraft: Dies gilt auch für alle von Ihnen aufgerufenen Bibliotheksfunktionen. Sie haben keine Kontrolle darüber, was Data.List.groupBy tun wird. Sie haben nur eine vernünftige "Garantie" / Konvention, dass die Versionsnummern etwas über die Kompatibilität aussagen. Es ist eine etwas andere Sache der Veränderung, wenn.
Hier zahlt sich die Verwendung von null aus - Sie versionieren bereits die generierten Dateien -, sodass Sie immer wissen, wann sich die Form des generierten Codes geändert hat. Das Betrachten der Unterschiede kann jedoch für große Mengen an generiertem Code etwas knorrig sein. Dies ist also ein Ort, an dem eine bessere Entwickleroberfläche nützlich wäre.
RE-Monolithismus: Sie können die Ergebnisse eines TH-Ausdrucks mit Ihrem eigenen Code zur Kompilierungszeit nachbearbeiten. Es wäre nicht sehr viel Code, nach Deklarationstyp / -name der obersten Ebene zu filtern. Sie können sich vorstellen, eine Funktion zu schreiben, die dies generisch tut. Zum Modifizieren / Entmonolithisieren von Quasiquotern können Sie die Musterübereinstimmung auf "QuasiQuoter" durchführen und die verwendeten Transformationen extrahieren oder eine neue in Bezug auf die alten erstellen.
quelle
[Dec]
und Dinge entfernen, die Sie nicht möchten, aber sagen wir, dass die Funktion beim Generieren der JSON-Schnittstelle eine externe Definitionsdatei liest. Nur weil Sie dies nicht verwenden,Dec
hört der Generator nicht auf, nach der Definitionsdatei zu suchen, und die Kompilierung schlägt fehl. Aus diesem Grund wäre es schön, eine restriktivere Version derQ
Monade zu haben, mit der Sie neue Namen (und solche Dinge) generieren, aber nicht zulassen könnenIO
, damit Sie, wie Sie sagen, die Ergebnisse filtern / andere Kompositionen erstellen können .Diese Antwort ist eine Antwort auf die von illissius Punkt für Punkt aufgeworfenen Fragen:
Genau. Ich habe das Gefühl, dass $ () so ausgewählt wurde, dass es Teil der Sprache ist - unter Verwendung der bekannten Symbolpalette von Haskell. Genau das möchten Sie jedoch nicht in den Symbolen, die für das Makrospleißen verwendet werden. Sie passen definitiv zu viel zusammen, und dieser kosmetische Aspekt ist ziemlich wichtig. Ich mag das Aussehen von {{}} für Spleiße, weil sie optisch sehr unterschiedlich sind.
Ich stimme dem jedoch auch zu, wie einige der Kommentare in "New Directions for TH" feststellen, ist das Fehlen einer guten sofort einsatzbereiten AST-Zitierung kein kritischer Fehler. In diesem WIP-Paket möchte ich diese Probleme in Bibliotheksform beheben: https://github.com/mgsloan/quasi-extras . Bisher erlaube ich das Spleißen an einigen Stellen mehr als gewöhnlich und kann Muster auf ASTs abgleichen.
Ich bin auf das Problem gestoßen, dass zyklische TH-Definitionen vorher unmöglich waren ... Es ist ziemlich ärgerlich. Es gibt eine Lösung, aber sie ist hässlich - packen Sie die an der zyklischen Abhängigkeit beteiligten Dinge in einen TH-Ausdruck ein, der alle generierten Deklarationen kombiniert. Einer dieser Deklarationsgeneratoren könnte nur ein Quasi-Quoter sein, der Haskell-Code akzeptiert.
Es ist nur prinzipienlos, wenn Sie prinzipienlose Dinge damit machen. Der einzige Unterschied besteht darin, dass Sie mit den vom Compiler implementierten Abstraktionsmechanismen mehr Vertrauen haben, dass die Abstraktion nicht undicht ist. Vielleicht klingt die Demokratisierung des Sprachdesigns ein bisschen beängstigend! Ersteller von TH-Bibliotheken müssen die Bedeutung und die Ergebnisse der von ihnen bereitgestellten Tools gut dokumentieren und klar definieren. Ein gutes Beispiel für prinzipielles TH ist das Ableitungspaket: http://hackage.haskell.org/package/derive - es verwendet eine DSL, so dass das Beispiel vieler Ableitungen / spezifiziert / die tatsächliche Ableitung.
Das ist ein ziemlich guter Punkt - die TH-API ist ziemlich groß und klobig. Eine Neuimplementierung scheint schwierig zu sein. Es gibt jedoch nur wenige Möglichkeiten, das Problem der Darstellung von Haskell-ASTs zu lösen. Ich stelle mir vor, dass Sie durch Kopieren der TH-ADTs und Schreiben eines Konverters in die interne AST-Darstellung einen guten Weg dorthin finden. Dies wäre gleichbedeutend mit dem (nicht unbedeutenden) Aufwand, haskell-src-meta zu erstellen. Es könnte auch einfach neu implementiert werden, indem der TH AST hübsch gedruckt und der interne Parser des Compilers verwendet wird.
Obwohl ich mich irren könnte, sehe ich TH aus Sicht der Implementierung nicht als so kompliziert wie eine Compiler-Erweiterung an. Dies ist tatsächlich einer der Vorteile des "Einfachhaltens" und des Fehlens der Grundschicht als theoretisch ansprechendes, statisch überprüfbares Schablonensystem.
Dies ist auch ein guter Punkt, aber etwas dramatisch. Zwar gab es in letzter Zeit API-Ergänzungen, diese haben jedoch nicht in großem Umfang zu Brüchen geführt. Ich denke auch, dass mit dem überlegenen AST-Zitat, das ich zuvor erwähnt habe, die API, die tatsächlich verwendet werden muss, sehr stark reduziert werden kann. Wenn keine Konstruktion / Übereinstimmung unterschiedliche Funktionen benötigt und stattdessen als Literale ausgedrückt wird, verschwindet der größte Teil der API. Darüber hinaus lässt sich der von Ihnen geschriebene Code leichter auf AST-Darstellungen für Sprachen portieren, die Haskell ähnlich sind.
Zusammenfassend denke ich, dass TH ein mächtiges, halb vernachlässigtes Werkzeug ist. Weniger Hass könnte zu einem lebendigeren Ökosystem von Bibliotheken führen und die Implementierung von Prototypen mit mehr Sprachmerkmalen fördern. Es wurde beobachtet, dass TH ein übermächtiges Werkzeug ist, mit dem Sie fast alles tun können. Anarchie! Nun, ich bin der Meinung, dass Sie mit dieser Fähigkeit die meisten ihrer Einschränkungen überwinden und Systeme konstruieren können, die zu prinzipiellen Metaprogrammierungsansätzen fähig sind. Es lohnt sich, hässliche Hacks zu verwenden, um die "richtige" Implementierung zu simulieren, da auf diese Weise das Design der "richtigen" Implementierung allmählich klar wird.
In meiner persönlichen idealen Version des Nirvana würde ein Großteil der Sprache tatsächlich aus dem Compiler in Bibliotheken dieser Art verschoben. Die Tatsache, dass die Funktionen als Bibliotheken implementiert sind, hat keinen großen Einfluss auf ihre Fähigkeit, originalgetreu zu abstrahieren.
Was ist die typische Antwort von Haskell auf Boilerplate-Code? Abstraktion. Was sind unsere Lieblingsabstraktionen? Funktionen und Typenklassen!
Mit Typklassen können wir eine Reihe von Methoden definieren, die dann in allen Arten von Funktionen verwendet werden können, die für diese Klasse generisch sind. Abgesehen davon können Klassen Boilerplate nur vermeiden, indem sie "Standarddefinitionen" anbieten. Hier ist ein Beispiel für eine prinzipienlose Funktion!
Minimale Bindungssätze sind nicht deklarierbar / vom Compiler überprüfbar. Dies könnte zu unbeabsichtigten Definitionen führen, die aufgrund gegenseitiger Rekursion den Boden ergeben.
Trotz der großen Bequemlichkeit und Leistung dieser ergeben würde, können Sie keine übergeordnete Klasse Standardwerte angeben, aufgrund orphan Instanzen http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Diese würden wir uns das beheben numerische Hierarchie anmutig!
Die Suche nach TH-ähnlichen Funktionen für Methodenstandards führte zu http://www.haskell.org/haskellwiki/GHC.Generics . Obwohl dies cool ist, war meine einzige Erfahrung mit dem Debuggen von Code mit diesen Generika nahezu unmöglich, da der Typ für ADT induziert wurde und ADT so kompliziert wie ein AST ist. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
Mit anderen Worten, dies ging nach den von TH bereitgestellten Merkmalen, aber es musste eine ganze Domäne der Sprache, die Konstruktionssprache, in eine Typensystemdarstellung heben. Ich kann zwar sehen, dass es für Ihr häufig auftretendes Problem gut funktioniert, aber für komplexe Probleme scheint es anfällig zu sein, einen Stapel von Symbolen zu liefern, der weitaus schrecklicher ist als TH-Hackery.
Mit TH können Sie den Ausgabecode zur Kompilierungszeit auf Wertebene berechnen, während Generics Sie dazu zwingt, den Mustervergleichs- / Rekursionsteil des Codes in das Typsystem zu heben. Dies schränkt den Benutzer zwar auf einige ziemlich nützliche Arten ein, aber ich denke nicht, dass sich die Komplexität lohnt.
Ich denke, dass die Ablehnung von TH und lisp-artiger Metaprogrammierung dazu geführt hat, dass Dinge wie Methodenvorgaben anstelle einer flexibleren Makro-Erweiterung wie Instanzdeklarationen bevorzugt wurden. Die Disziplin, Dinge zu vermeiden, die zu unvorhergesehenen Ergebnissen führen könnten, ist klug. Wir sollten jedoch nicht ignorieren, dass das leistungsfähige Typsystem von Haskell eine zuverlässigere Metaprogrammierung ermöglicht als in vielen anderen Umgebungen (durch Überprüfen des generierten Codes).
quelle
Ein eher pragmatisches Problem bei Template Haskell ist, dass es nur funktioniert, wenn der Bytecode-Interpreter von GHC verfügbar ist, was nicht bei allen Architekturen der Fall ist. Wenn Ihr Programm Template Haskell verwendet oder auf Bibliotheken angewiesen ist, die es verwenden, wird es nicht auf Computern mit einer ARM-, MIPS-, S390- oder PowerPC-CPU ausgeführt.
Dies ist in der Praxis relevant: git-annex ist ein in Haskell geschriebenes Tool, das sinnvoll ist, um auf Computern ausgeführt zu werden, die sich um Speicher sorgen. Solche Computer verfügen häufig über Nicht-i386-CPUs. Persönlich führe ich git-annex auf einer NSLU 2 aus (32 MB RAM, 266 MHz CPU; wussten Sie, dass Haskell auf solcher Hardware gut funktioniert?). Wenn Template Haskell verwendet wird, ist dies nicht möglich.
(Die Situation über GHC auf ARM verbessert sich heutzutage sehr und ich denke, 7.4.2 funktioniert sogar, aber der Punkt bleibt bestehen).
quelle
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)Warum ist TH schlecht? Für mich kommt es darauf an:
Denk darüber nach. Der halbe Reiz von Haskell besteht darin, dass Sie durch sein hochwertiges Design große Mengen an nutzlosem Boilerplate-Code vermeiden können, den Sie in anderen Sprachen schreiben müssen. Wenn Sie eine Codegenerierung zur Kompilierungszeit benötigen , sagen Sie im Grunde, dass entweder Ihre Sprache oder Ihr Anwendungsdesign versagt hat. Und wir Programmierer scheitern nicht gern.
Manchmal ist es natürlich notwendig. Aber manchmal können Sie vermeiden, TH zu benötigen, indem Sie mit Ihren Designs nur ein bisschen schlauer umgehen.
(Die andere Sache ist, dass TH ziemlich niedrig ist. Es gibt kein großartiges Design auf hoher Ebene; viele interne Implementierungsdetails von GHC werden offengelegt. Und das macht die API anfällig für Änderungen ...)
quelle