Parece que Template Haskell es a menudo visto por la comunidad de Haskell como una conveniencia desafortunada. Es difícil poner en palabras exactamente lo que he observado al respecto, pero considere estos pocos ejemplos
- La plantilla Haskell aparece en "Lo feo (pero necesario)" en respuesta a la pregunta ¿Qué extensiones de Haskell (GHC) deben usar / evitar los usuarios?
- La plantilla Haskell consideró una solución temporal / inferior en los vectores sin caja del hilo de valores newtype'd (lista de correo de bibliotecas)
- Yesod es a menudo criticado por confiar demasiado en Template Haskell (ver la publicación del blog en respuesta a este sentimiento)
He visto varias publicaciones de blog en las que las personas hacen cosas bastante buenas con Template Haskell, lo que permite una sintaxis más bonita que simplemente no sería posible en Haskell regular, así como una tremenda reducción de repeticiones. Entonces, ¿por qué se desprecia a Template Haskell de esta manera? ¿Qué lo hace indeseable? ¿En qué circunstancias se debe evitar Template Haskell y por qué?
haskell
template-haskell
Dan Burton
fuente
fuente
Respuestas:
Una razón para evitar Template Haskell es que, en general, no es de tipo seguro, lo que va en contra de gran parte del "espíritu de Haskell". Aquí hay algunos ejemplos de esto:
Exp
, pero no sabe si se trata de una expresión que representa un[Char]
o una(a -> (forall b . b -> c))
, o lo que sea. TH sería más confiable si se pudiera expresar que una función solo puede generar expresiones de cierto tipo, o solo declaraciones de funciones, o solo patrones de coincidencia de constructor de datos, etc.foo
que no existe? Mala suerte, solo verá eso cuando realmente use su generador de código, y solo bajo las circunstancias que desencadenan la generación de ese código en particular. También es muy difícil realizar pruebas unitarias.TH también es absolutamente peligroso:
IO
, incluido el lanzamiento de misiles o el robo de su tarjeta de crédito. No querrás tener que revisar cada paquete de cabal que hayas descargado en busca de exploits TH.Luego, hay algunos problemas que hacen que las funciones de TH sean menos divertidas de usar como desarrollador de bibliotecas:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
es solo una mónada, por lo que puede usar todas las funciones habituales en él. Algunas personas no saben esto, y debido a eso, crean múltiples versiones sobrecargadas de esencialmente las mismas funciones con la misma funcionalidad, y estas funciones conducen a un cierto efecto de hinchazón. Además, la mayoría de las personas escribe sus generadores en laQ
mónada incluso cuando no tienen que hacerlo, que es como escribirbla :: IO Int; bla = return 3
; le está dando a una función más "entorno" de lo que necesita, y los clientes de la función deben proporcionar ese entorno como efecto de eso.Finalmente, hay algunas cosas que hacen que las funciones de TH sean menos divertidas de usar como usuario final:
Q Dec
, puede generar absolutamente cualquier cosa en el nivel superior de un módulo, y no tiene absolutamente ningún control sobre lo que se generará.fuente
Esta es únicamente mi propia opinión.
Es feo de usar.
$(fooBar ''Asdf)
simplemente no se ve bien. Superficial, claro, pero contribuye.Es aún más feo escribir. Las citas a veces funcionan, pero muchas veces tienes que hacer un injerto y plomería AST manual. La API es grande y difícil de manejar, siempre hay muchos casos que no le importan pero que aún necesita despachar, y los casos que sí le interesan tienden a estar presentes en múltiples formas similares pero no idénticas (datos versus tipo nuevo, registro -constructor versus constructores normales, etc.). Es aburrido y repetitivo escribir y lo suficientemente complicado como para no ser mecánico. La propuesta de reforma aborda algo de esto (haciendo que las citas sean más aplicables).
La restricción de escenario es el infierno. No poder empalmar las funciones definidas en el mismo módulo es la parte más pequeña del mismo: la otra consecuencia es que si tiene un empalme de nivel superior, todo lo que esté después en el módulo estará fuera del alcance de cualquier cosa anterior. Otros lenguajes con esta propiedad (C, C ++) lo hacen viable al permitirle reenviar declaraciones, pero Haskell no. Si necesita referencias cíclicas entre declaraciones empalmadas o sus dependencias y dependientes, generalmente está jodido.
Es indisciplinado. Lo que quiero decir con esto es que la mayoría de las veces cuando expresas una abstracción, hay algún tipo de principio o concepto detrás de esa abstracción. Para muchas abstracciones, el principio detrás de ellas se puede expresar en sus tipos. Para las clases de tipos, a menudo puede formular leyes que las instancias deben obedecer y los clientes pueden asumir. Si utiliza la nueva función genérica de GHC para abstraer la forma de una declaración de instancia sobre cualquier tipo de datos (dentro de los límites), puede decir "para los tipos de suma, funciona así, para los tipos de productos, funciona así". Template Haskell, por otro lado, son solo macros. No es la abstracción a nivel de ideas, sino la abstracción a nivel de AST, que es mejor, pero solo modestamente, que la abstracción a nivel de texto plano. *
Te vincula a GHC. En teoría, otro compilador podría implementarlo, pero en la práctica dudo que esto suceda alguna vez. (Esto está en contraste con varias extensiones de sistema de tipo que, aunque solo podrían ser implementadas por GHC en este momento, fácilmente podría imaginar ser adoptado por otros compiladores en el futuro y eventualmente estandarizado).
La API no es estable. Cuando se agregan nuevas funciones de lenguaje a GHC y el paquete template-haskell se actualiza para admitirlas, esto a menudo implica cambios incompatibles con versiones anteriores de los tipos de datos TH. Si desea que su código TH sea compatible con más de una versión de GHC, debe tener mucho cuidado y posiblemente usarlo
CPP
.Hay un principio general de que debe usar la herramienta adecuada para el trabajo y la más pequeña que sea suficiente, y en esa analogía, Template Haskell es algo así . Si hay una manera de hacerlo que no sea Template Haskell, generalmente es preferible.
La ventaja de Template Haskell es que puedes hacer cosas que no podrías hacer de otra manera, y es muy importante. La mayoría de las veces, las cosas para las que se usa TH solo se podrían hacer si se implementaran directamente como características del compilador. TH es extremadamente beneficioso tener ambos porque le permite hacer estas cosas y porque le permite crear prototipos de posibles extensiones de compilador de una manera mucho más liviana y reutilizable (vea los diversos paquetes de lentes, por ejemplo).
Para resumir por qué creo que hay sentimientos negativos hacia Template Haskell: resuelve muchos problemas, pero para cualquier problema que resuelva, parece que debería haber una solución mejor, más elegante y disciplinada más adecuada para resolver ese problema, uno que no resuelva el problema generando automáticamente la placa de repeticiones, pero eliminando la necesidad de tener la placa de repeticiones.
* Aunque a menudo siento que
CPP
tiene una mejor relación potencia / peso para esos problemas que puede resolver.EDITAR 23-04-14: A lo que intentaba llegar con frecuencia en lo anterior, y que recientemente he llegado exactamente, es que hay una distinción importante entre abstracción y deduplicación. La abstracción adecuada a menudo resulta en la deduplicación como un efecto secundario, y la duplicación a menudo es un signo revelador de abstracción inadecuada, pero esa no es la razón por la cual es valioso. La abstracción adecuada es lo que hace que el código sea correcto, comprensible y mantenible. La deduplicación solo lo hace más corto. Template Haskell, como las macros en general, es una herramienta para la deduplicación.
fuente
Me gustaría abordar algunos de los puntos que plantea dflemstr.
No creo que el hecho de que no puedas escribir TH sea tan preocupante. ¿Por qué? Porque incluso si hay un error, seguirá siendo tiempo de compilación. No estoy seguro de si esto fortalece mi argumento, pero esto es similar en espíritu a los errores que recibe al usar plantillas en C ++. Sin embargo, creo que estos errores son más comprensibles que los errores de C ++, ya que obtendrá una versión bastante impresa del código generado.
Si una expresión TH / cuasi-quoter hace algo tan avanzado que las esquinas difíciles pueden esconderse, ¿entonces quizás sea desaconsejado?
Rompo esta regla bastante con las cuasi-citas en las que he estado trabajando últimamente (usando haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Sé que esto introduce algunos errores, como no poder empalmar en las comprensiones de la lista generalizada. Sin embargo, creo que hay una buena posibilidad de que algunas de las ideas en http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal terminen en el compilador. Hasta entonces, las bibliotecas para analizar los árboles de Haskell a TH son una aproximación casi perfecta.
Con respecto a la velocidad de compilación / dependencias, podemos usar el paquete "zeroth" para incorporar el código generado. Esto es al menos agradable para los usuarios de una biblioteca determinada, pero no podemos hacer mucho mejor para el caso de editar la biblioteca. ¿Pueden las dependencias TH hinchar binarios generados? Pensé que dejaba de lado todo lo que el código compilado no hace referencia.
La restricción / división de etapas de los pasos de compilación del módulo Haskell es una mierda.
Opacidad RE: Esto es lo mismo para cualquier función de biblioteca que llame. No tienes control sobre lo que hará Data.List.groupBy. Simplemente tiene una "garantía" / convención razonable de que los números de versión le dicen algo sobre la compatibilidad. Es una cuestión de cambio algo diferente cuando.
Aquí es donde vale la pena usar zeroth: ya está versionando los archivos generados, por lo que siempre sabrá cuándo ha cambiado la forma del código generado. Sin embargo, mirar las diferencias puede ser un poco retorcido para grandes cantidades de código generado, por lo que ese es un lugar donde una mejor interfaz de desarrollador sería útil.
Monolitismo RE: Ciertamente puede postprocesar los resultados de una expresión TH, utilizando su propio código de tiempo de compilación. No sería mucho código filtrar por tipo / nombre de declaración de nivel superior. Diablos, podrías imaginar escribir una función que haga esto genéricamente. Para modificar / des-monolitizar cuasiquoters, puede combinar patrones en "QuasiQuoter" y extraer las transformaciones utilizadas, o hacer una nueva en términos de la anterior.
fuente
[Dec]
ay eliminar cosas que no desea, pero supongamos que la función lee un archivo de definición externo al generar la interfaz JSON. El hecho de que no use esoDec
no hace que el generador deje de buscar el archivo de definición, lo que hace que la compilación falle. Por esa razón, sería bueno tener una versión más restrictiva de laQ
mónada que le permitiera generar nuevos nombres (y cosas así) pero no permitirIO
, de modo que, como usted dice, uno podría filtrar sus resultados / hacer otras composiciones .Esta respuesta responde a los problemas planteados por illissius, punto por punto:
Estoy de acuerdo. Siento que $ () se eligió para que pareciera que era parte del lenguaje, utilizando la paleta de símbolos familiar de Haskell. Sin embargo, eso es exactamente lo que usted / no / quiere en los símbolos utilizados para su empalme de macro. Definitivamente se mezclan demasiado, y este aspecto cosmético es bastante importante. Me gusta la apariencia de {{}} para los empalmes, porque son bastante distintos visualmente.
Sin embargo, también estoy de acuerdo con esto, como lo observan algunos de los comentarios en "Nuevas direcciones para TH", la falta de una buena cita de AST lista para usar no es un defecto crítico. En este paquete WIP, busco abordar estos problemas en forma de biblioteca: https://github.com/mgsloan/quasi-extras . Hasta ahora, permito empalmar en algunos lugares más de lo habitual y puedo hacer coincidir patrones en AST.
Me he encontrado con el problema de que las definiciones cíclicas de TH son imposibles antes ... Es bastante molesto. Hay una solución, pero es fea: envolver las cosas involucradas en la dependencia cíclica en una expresión TH que combine todas las declaraciones generadas. Uno de estos generadores de declaraciones podría ser un cuasi-cita que acepta el código Haskell.
Solo no tiene principios si haces cosas sin principios con él. La única diferencia es que con los mecanismos de abstracción implementados por el compilador, tiene más confianza en que la abstracción no tiene fugas. ¡Quizás democratizar el diseño del lenguaje suena un poco aterrador! Los creadores de las bibliotecas TH necesitan documentar bien y definir claramente el significado y los resultados de las herramientas que proporcionan. Un buen ejemplo de TH basado en principios es el paquete de derivación : http://hackage.haskell.org/package/derive : utiliza un DSL tal que el ejemplo de muchas de las derivaciones / especifica / la derivación real.
Ese es un punto bastante bueno: la API TH es bastante grande y torpe. Volver a implementarlo parece que podría ser difícil. Sin embargo, en realidad solo hay algunas maneras de dividir el problema de representar AST de Haskell. Me imagino que copiar los TH ADT y escribir un convertidor en la representación interna de AST te ayudaría mucho. Esto sería equivalente al esfuerzo (no insignificante) de crear haskell-src-meta. También se podría volver a implementar simplemente imprimiendo bonito el TH AST y utilizando el analizador interno del compilador.
Si bien podría estar equivocado, no veo a TH como una extensión de compilador tan complicada, desde una perspectiva de implementación. Este es en realidad uno de los beneficios de "mantenerlo simple" y no tener la capa fundamental como un sistema de plantillas teóricamente atractivo y verificable estáticamente.
Este también es un buen punto, pero algo dramático. Si bien ha habido adiciones de API últimamente, no han sido ampliamente inductoras de roturas. Además, creo que con la cita superior de AST que mencioné anteriormente, la API que realmente necesita ser utilizada puede reducirse sustancialmente. Si ninguna construcción / coincidencia necesita funciones distintas, y en su lugar se expresan como literales, entonces la mayor parte de la API desaparece. Además, el código que escriba se transferirá más fácilmente a las representaciones de AST para idiomas similares a Haskell.
En resumen, creo que TH es una herramienta poderosa, semi-descuidada. Menos odio podría conducir a un ecosistema de bibliotecas más animado, fomentando la implementación de más prototipos de características del lenguaje. Se ha observado que TH es una herramienta dominada, que puede permitirte / hacer / casi cualquier cosa. ¡Anarquía! Bueno, es mi opinión que este poder puede permitirle superar la mayoría de sus limitaciones y construir sistemas capaces de enfoques de metaprogramación bastante basados en principios. Vale la pena usar hacks feos para simular la implementación "adecuada", ya que de esta manera el diseño de la implementación "adecuada" se irá aclarando gradualmente.
En mi versión ideal personal de nirvana, gran parte del lenguaje se movería del compilador a bibliotecas de esta variedad. El hecho de que las características se implementen como bibliotecas no influye mucho en su capacidad de abstraer fielmente.
¿Cuál es la respuesta típica de Haskell al código repetitivo? Abstracción. ¿Cuáles son nuestras abstracciones favoritas? Funciones y clases de tipos!
Las clases de tipos nos permiten definir un conjunto de métodos, que luego se pueden utilizar en todo tipo de funciones genéricas en esa clase. Sin embargo, aparte de esto, la única forma en que las clases ayudan a evitar repeticiones es ofreciendo "definiciones predeterminadas". ¡Ahora aquí hay un ejemplo de una característica sin principios!
Los conjuntos de enlace mínimos no son declarables / compilador comprobable. Esto podría conducir a definiciones inadvertidas que rinden fondo debido a la recursividad mutua.
A pesar de la gran comodidad y potencia que esto produciría, no puede especificar valores predeterminados de superclase, debido a instancias huérfanas http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Esto nos permitiría arreglar el jerarquía numérica con gracia!
Buscar las capacidades de TH para los valores predeterminados de los métodos condujo a http://www.haskell.org/haskellwiki/GHC.Generics . Si bien esto es algo genial, mi única experiencia en la depuración de código usando estos genéricos fue casi imposible, debido al tamaño del tipo inducido y ADT tan complicado como un AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
En otras palabras, esto fue después de las características proporcionadas por TH, pero tuvo que levantar un dominio completo del lenguaje, el lenguaje de construcción, en una representación de sistema de tipos. Si bien puedo ver que funciona bien para su problema común, para los complejos, parece propenso a producir una pila de símbolos mucho más aterrador que la piratería informática TH.
TH le proporciona un cálculo en tiempo de compilación a nivel de valor del código de salida, mientras que los genéricos lo obligan a levantar la parte de coincidencia / recursión de patrones del código en el sistema de tipos. Si bien esto restringe al usuario de algunas maneras bastante útiles, no creo que la complejidad valga la pena.
Creo que el rechazo de TH y la metaprogramación de tipo lisp condujo a la preferencia hacia cosas como los valores predeterminados del método en lugar de declaraciones de instancias más flexibles y de macroexpansión. La disciplina de evitar cosas que podrían conducir a resultados imprevistos es sabia, sin embargo, no debemos ignorar que el sistema de tipos capaz de Haskell permite una metaprogramación más confiable que en muchos otros entornos (verificando el código generado).
fuente
Un problema bastante pragmático con Template Haskell es que solo funciona cuando el intérprete de bytecode de GHC está disponible, lo cual no es el caso en todas las arquitecturas. Entonces, si su programa usa Template Haskell o se basa en bibliotecas que lo usan, no se ejecutará en máquinas con una CPU ARM, MIPS, S390 o PowerPC.
Esto es relevante en la práctica: git-annex es una herramienta escrita en Haskell que tiene sentido para ejecutarse en máquinas que se preocupan por el almacenamiento, estas máquinas a menudo tienen CPU no i386. Personalmente, ejecuto git-annex en un NSLU 2 (32 MB de RAM, CPU de 266MHz; ¿sabías que Haskell funciona bien en dicho hardware?) Si usaría Template Haskell, esto no es posible.
(La situación sobre GHC en ARM está mejorando mucho en estos días y creo que 7.4.2 incluso funciona, pero el punto sigue en pie).
fuente
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)¿Por qué es malo TH? Para mí, todo se reduce a esto:
Piénsalo. La mitad del atractivo de Haskell es que su diseño de alto nivel le permite evitar grandes cantidades de código inútil que debe escribir en otros idiomas. Si necesita generar código en tiempo de compilación, básicamente está diciendo que su idioma o el diseño de su aplicación le han fallado. Y a los programadores no nos gusta fallar.
A veces, por supuesto, es necesario. Pero a veces puede evitar necesitar TH simplemente siendo un poco más inteligente con sus diseños.
(La otra cosa es que TH es de nivel bastante bajo. No hay un gran diseño de alto nivel; muchos detalles de implementación interna de GHC están expuestos. Y eso hace que la API sea propensa a cambiar ...)
fuente