haskell template-haskell

¿Qué tiene de malo Template Haskell?



template-haskell (6)

Parece que Template Haskell a menudo es visto por la comunidad de Haskell como una conveniencia desafortunada. Es difícil poner en palabras exactamente lo que he observado a este respecto, pero considere estos pocos ejemplos.

  • La plantilla Haskell aparece en "The Ugly (pero necesario)" en respuesta a la pregunta ¿Qué extensiones Haskell (GHC) deben usar / evitar los usuarios?
  • La plantilla Haskell consideró una solución temporal / inferior en el vector Unboxed de hilos de valores newtype''d (lista de correo de bibliotecas)
  • Yesod es a menudo criticado por confiar demasiado en la plantilla Haskell (consulte la publicación del blog en respuesta a este sentimiento)

He visto varias publicaciones de blog en las que la gente hace cosas muy buenas con Template Haskell, lo que permite una sintaxis más bonita que simplemente no sería posible en Haskell normal, así como una tremenda reducción de la plantilla. Entonces, ¿por qué es que la Plantilla Haskell es menospreciada de esta manera? ¿Qué lo hace indeseable? ¿En qué circunstancias debe evitarse Template Haskell y por qué?


¿Por qué es malo TH? Para mí, todo se reduce a esto:

Si necesita producir tanto código repetitivo que intenta utilizar TH para generarlo automáticamente, ¡ lo está haciendo mal!

Piénsalo. La mitad del atractivo de Haskell es que su diseño de alto nivel le permite evitar enormes cantidades de código inútil que tiene que escribir en otros idiomas. Si necesita generación de 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 puedes evitar necesitar TH simplemente siendo un poco más inteligente con tus diseños.

(La otra cosa es que el nivel de TH es bastante bajo. No hay un diseño de gran nivel de alto nivel; muchos de los detalles de implementación interna de GHC están expuestos. Y eso hace que la API sea propensa a cambiar ...)


Esta es únicamente mi propia opinión.

  • Es feo de usar. $(fooBar ''''Asdf) simplemente no se ve bien. Superficial, claro, pero aporta.

  • Es aún más feo escribir. La cotización a veces funciona, pero la mayor parte del tiempo tiene que hacer el injerto manual de AST y la plomería. La API es grande y difícil de manejar, siempre hay una gran cantidad de casos que no le interesan pero que aún deben enviarse, y los casos que sí le interesan tienden a estar presentes en múltiples formas similares pero no idénticas (data vs. newtype, record -constructores de estilo vs normales, y así sucesivamente). Es aburrido y repetitivo escribir y lo suficientemente complicado como para no ser mecánico. La propuesta de reforma aborda parte de esto (haciendo que las citas sean más aplicables).

  • La restricción de etapa es el infierno. No poder empalmar las funciones definidas en el mismo módulo es la parte más pequeña de la misma: 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 idiomas con esta propiedad (C, C ++) lo hacen viable al permitirle reenviar declaraciones, pero Haskell no lo hace. Si necesita referencias cíclicas entre las declaraciones empalmadas o sus dependencias y dependientes, por lo general solo 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 puede expresarse en sus tipos. Para las clases de tipos, a menudo puede formular leyes que las instancias deben obedecer y los clientes pueden asumir. Si usa la nueva característica 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 de esta manera, para los tipos de productos, funciona de esa manera". La plantilla Haskell, por otro lado, es solo macros. No es una abstracción a nivel de ideas, sino una abstracción a nivel de AST, que es mejor, pero solo modestamente, que la abstracción a nivel de texto sin formato. *

  • Te ata a GHC. En teoría, otro compilador podría implementarlo, pero en la práctica dudo que esto suceda alguna vez. (Esto contrasta con varios tipos de extensiones de sistema que, aunque en este momento solo podrían ser implementadas por GHC, podría imaginarme fácilmente que fueron adoptadas por otros compiladores en el camino y eventualmente estandarizadas).

  • La API no es estable. Cuando se agregan nuevas características de lenguaje a GHC y el paquete de plantilla-haskell se actualiza para admitirlas, esto a menudo implica cambios incompatibles con los tipos de datos TH. Si desea que su código TH sea compatible con más de una versión de GHC, debe ser muy cuidadoso y posiblemente usar CPP .

  • Hay un principio general de que deberías usar la herramienta correcta para el trabajo y la más pequeña que sea suficiente, y en esa analogía, la Plantilla Haskell es algo como esto . Si hay una forma de hacerlo que no sea Template Haskell, generalmente es preferible.

La ventaja de Template Haskell es que puedes hacer cosas con él que no podrías hacer de otra manera, y es una gran cosa. La mayoría de las veces, las cosas para las que se usa TH se podrían hacer de otra manera si se implementaran directamente como características del compilador. TH es extremadamente beneficioso al tener ambos porque le permite hacer estas cosas y porque le permite crear prototipos de extensiones de compilador potenciales de una manera mucho más liviana y reutilizable (consulte los distintos 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 resuelve el problema generando automáticamente la placa de caldera, pero eliminando la necesidad de tener la placa de caldera.

* Aunque a menudo siento que CPP tiene una mejor relación potencia / peso para los problemas que puede resolver.

EDICIÓN 23-04-14: Lo que intentaba tratar con frecuencia en lo que antecede, y recientemente lo he conseguido 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 efecto secundario, y la duplicación es a menudo un signo revelador de abstracción inadecuada, pero esa no es la razón por la que es valiosa. 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. La plantilla Haskell, como las macros en general, es una herramienta para la deduplicación.


Esta respuesta responde a las cuestiones planteadas por illissius, punto por punto:

  • Es feo de usar. $ (fooBar '''' Asdf) simplemente no se ve bien. Superficial, claro, pero aporta.

Estoy de acuerdo. Siento que se eligió a $ () para que pareciera que era parte del lenguaje, utilizando el símbolo de la paleta de Haskell. Sin embargo, eso es exactamente lo que usted / no / quiere en los símbolos utilizados para su empalme de macros. Definitivamente se mezclan demasiado, y este aspecto cosmético es bastante importante. Me gusta el aspecto de {{}} para los empalmes, porque son muy distintos visualmente.

  • Es aún más feo escribir. La cotización a veces funciona, pero la mayor parte del tiempo tiene que hacer el injerto manual de AST y la plomería. El [API] [1] es grande y difícil de manejar, siempre hay muchos casos que no le interesan pero que aún deben enviarse, y los casos que sí le interesan tienden a estar presentes en múltiples formas similares pero no idénticas (datos). vs. newtype, estilo de registro vs. constructores normales, y así sucesivamente). Es aburrido y repetitivo escribir y lo suficientemente complicado como para no ser mecánico. La [propuesta de reforma] [2] aborda parte de esto (haciendo que las citas sean más aplicables).

Sin embargo, también estoy de acuerdo con esto, como observan algunos de los comentarios en "Nuevas instrucciones para TH", la falta de una buena cita de AST no es un defecto crítico. En este paquete WIP, busco resolver 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 los patrones en los AST.

  • La restricción de etapa es el infierno. No poder empalmar las funciones definidas en el mismo módulo es la parte más pequeña de la misma: 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 idiomas con esta propiedad (C, C ++) lo hacen viable al permitirle reenviar declaraciones, pero Haskell no lo hace. Si necesita referencias cíclicas entre las declaraciones empalmadas o sus dependencias y dependientes, por lo general solo está jodido.

Me he topado con el problema de que las definiciones de TH cíclicas son imposibles antes ... Es bastante molesto. Hay una solución, pero es fea: envuelva las cosas involucradas en la dependencia cíclica en una expresión de TH que combine todas las declaraciones generadas. Uno de estos generadores de declaraciones podría ser un cuasi-quoter que acepte el código Haskell.

  • No tiene principios. 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 puede expresarse en sus tipos. Cuando define una clase de tipo, a menudo puede formular leyes que las instancias deben obedecer y los clientes pueden asumir. Si usa la [nueva característica genérica] [3] 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í". ". Pero Template Haskell es solo macros tonto. No es una abstracción a nivel de ideas, sino una abstracción a nivel de AST, que es mejor, pero solo modestamente, que la abstracción a nivel de texto sin formato.

Es solo sin principios si haces cosas sin principios con eso. La única diferencia es que con el compilador implementado mecanismos para la abstracción, tiene más confianza en que la abstracción no tiene fugas. ¡Tal vez la democratización del diseño lingüístico suene un poco aterrador! Los creadores de las bibliotecas de TH deben documentar bien y definir claramente el significado y los resultados de las herramientas que proporcionan. Un buen ejemplo de los principios de TH es el paquete derivado: http://hackage.haskell.org/package/derive : utiliza un DSL de tal forma que el ejemplo de muchas de las derivaciones / especifica / la derivación real.

  • Te ata a GHC. En teoría, otro compilador podría implementarlo, pero en la práctica dudo que esto suceda alguna vez. (Esto contrasta con varios tipos de extensiones de sistema que, aunque en este momento solo podrían ser implementadas por GHC, podría imaginarme fácilmente que fueron adoptadas por otros compiladores en el camino y eventualmente estandarizadas).

Ese es un punto bastante bueno: la API de TH es bastante grande y torpe. Re-implementarlo parece que podría ser difícil. Sin embargo, solo hay unas pocas formas de resolver el problema de representar los AST de Haskell. Me imagino que copiar los ADT de TH, y escribir un convertidor a la representación interna de AST te ayudaría a llegar hasta allí. Esto sería equivalente al esfuerzo (no insignificante) de crear haskell-src-meta. También se puede volver a implementar simplemente imprimiendo bastante el TH AST y utilizando el analizador interno del compilador.

Si bien podría estar equivocado, no veo que TH sea tan complicado como una extensión de compilador, desde una perspectiva de implementación. Este es en realidad uno de los beneficios de "mantenerlo simple" y de no tener la capa fundamental como un sistema de plantillas teóricamente atractivo y estáticamente verificable.

  • La API no es estable. Cuando se agregan nuevas características de lenguaje a GHC y el paquete de plantilla-haskell se actualiza para admitirlas, esto a menudo implica cambios incompatibles con los tipos de datos TH. Si desea que su código TH sea compatible con más de una versión de GHC, debe ser muy cuidadoso y posiblemente usar CPP .

Este es también un buen punto, pero algo dramatizado. Si bien últimamente se han agregado adiciones a la API, no han sido extensivamente inductores de roturas. Además, creo que con la cita de AST superior 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, la mayoría de las API desaparecen. Además, el código que escribas se trasladaría más fácilmente a las representaciones AST para lenguajes similares a Haskell.

En resumen, creo que TH es una herramienta poderosa, semi-descuidada. Menos odio podría llevar a un ecosistema de bibliotecas más animado, alentando la implementación de más prototipos de características de lenguaje. Se ha observado que la TH es una herramienta superada, que le permite / hacer / casi cualquier cosa. ¡Anarquía! Bueno, en mi opinión, este poder puede permitirle superar la mayoría de sus limitaciones y construir sistemas capaces de enfoques de meta-programación con bastante principios. Vale la pena el uso de hacks feos para simular la implementación "adecuada", ya que de esta manera el diseño de la implementación "adecuada" se volverá gradualmente claro.

En mi versión ideal personal de nirvana, gran parte del lenguaje en realidad se movería fuera del compilador hacia bibliotecas de esta variedad. El hecho de que las funciones se implementen como bibliotecas no influye mucho en su capacidad para abstraerse fielmente.

¿Cuál es la respuesta típica de Haskell al código repetitivo? Abstracción. ¿Cuáles son nuestras abstracciones favoritas? Funciones y tipos de clases!

Typeclasses nos permite definir un conjunto de métodos, que luego se pueden utilizar en todas las funciones genéricas de esa clase. Sin embargo, aparte de esto, la única forma en que las clases ayudan a evitar la repetición es ofreciendo "definiciones predeterminadas". ¡Aquí hay un ejemplo de una característica sin principios!

  • Los conjuntos de enlaces mínimos no son declarables / compilables verificables. Esto podría llevar a definiciones inadvertidas que arrojen un fondo debido a la recursión mutua.

  • A pesar de la gran conveniencia y potencia que esto proporcionaría, no puede especificar los valores predeterminados de superclase, debido a las instancias de huérfanos http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Esto nos permitiría arreglar el jerarquía numérica con gracia!

  • La búsqueda de capacidades similares a las de TH para los valores predeterminados de los métodos llevó a http://www.haskell.org/haskellwiki/GHC.Generics . Si bien esto es algo genial, mi única experiencia en el código de depuración con estos genéricos fue casi imposible, debido al tamaño del tipo inducido para 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 aterradora que la piratería de TH.

    TH le proporciona el cálculo de tiempo de compilación del código de salida, mientras que los genéricos lo obligan a levantar la parte del código de coincidencia / recursión del patrón en el sistema de tipos. Si bien esto restringe al usuario de algunas maneras bastante útiles, no creo que la complejidad valga la pena.

Pienso que el rechazo de la metaprogramación de TH y el estilo lisp llevó a la preferencia hacia cosas como valores por defecto de métodos en lugar de más macro, macro-expansión como declaraciones de instancias. La disciplina de evitar cosas que podrían conducir a resultados imprevistos es sabia, sin embargo, no debemos ignorar que el sistema de tipo capaz de Haskell permite una metaprogramación más confiable que en muchos otros entornos (al verificar el código generado).


Me gustaría abordar algunos de los puntos que dflemstr trae.

No encuentro el hecho de que no puedas verificar que TH sea tan preocupante. ¿Por qué? Porque incluso si hay un error, todavía será tiempo de compilación. No estoy seguro de si esto refuerza mi argumento, pero es similar en espíritu a los errores que recibe cuando usa plantillas en C ++. Sin embargo, creo que estos errores son más comprensibles que los errores de C ++, ya que obtendrás una versión bastante impresa del código generado.

Si una expresión / cuasi-quiste de TH hace algo que es tan avanzado que los rincones difíciles pueden ocultar, entonces ¿tal vez no es aconsejable?

Rompo esta regla bastante con los cuasi cotizadores en los que he estado trabajando últimamente (utilizando haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Sé que esto introduce algunos errores, como no poder unir en la lista generalizada de comprensión. Sin embargo, creo que hay una buena probabilidad 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 Haskell a los árboles TH son una aproximación casi perfecta.

Con respecto a la velocidad de compilación / dependencias, podemos usar el paquete "zeroth" para alinear el código generado. Esto es al menos agradable para los usuarios de una biblioteca determinada, pero no podemos hacerlo mucho mejor en el caso de editar la biblioteca. ¿Pueden las dependencias TH inflar los binarios generados? Pensé que había omitido todo lo que no está referenciado por el código compilado.

La restricción / división de etapas de los pasos de compilación del módulo Haskell apesta.

Opacidad de RE: es la misma para cualquier función de biblioteca a la 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 algo así como una cuestión diferente de cambio cuando.

Aquí es donde el uso de zeroth se amortiza (ya está versionando los archivos generados), por lo que siempre sabrá cuándo ha cambiado la forma del código generado. Sin embargo, observar 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 para filtrar en el tipo / nombre de declaración de nivel superior. Diablos, podrías imaginarte escribiendo una función que haga esto genéricamente. Para modificar / desmonolizar los cuasiquoters, puede hacer un patrón de coincidencia en "QuasiQuoter" y extraer las transformaciones utilizadas, o hacer una nueva en términos de la antigua.


Un problema bastante pragmático con Template Haskell es que solo funciona cuando el intérprete de bytecode de GHC está disponible, lo que no es el caso en todas las arquitecturas. Entonces, si su programa usa Template Haskell o confía en las 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 preocupadas por el almacenamiento, tales máquinas a menudo tienen CPU que no son i386. Personalmente, ejecuto git-annex en un NSLU 2 (32 MB de RAM, 266MHz de CPU; ¿sabía que Haskell funciona bien en dicho hardware?) Si usara 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).


Una de las razones para evitar Template Haskell es que, en conjunto, no es del todo seguro para el tipo, por lo que va en contra de gran parte del "espíritu de Haskell". Aquí hay algunos ejemplos de esto:

  • No tiene control sobre qué tipo de Haskell AST generará un código TH, más allá de donde aparecerá; puede tener un valor de tipo Exp , pero no sabe si es una expresión que representa a [Char] o a (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 constructores de datos, etc.
  • Puedes generar expresiones que no se compilen. ¿Generó una expresión que hace referencia a una variable libre foo que no existe? Suerte, solo verá eso cuando use su generador de código, y solo en las circunstancias que activan la generación de ese código en particular. También es muy difícil realizar la prueba unitaria.

TH también es francamente peligroso:

  • El código que se ejecuta en tiempo de compilación puede hacer IO / IO arbitrarias, incluido el lanzamiento de misiles o el robo de su tarjeta de crédito. No querrás tener que mirar a través de cada paquete cabal que alguna vez descargues en busca de exploits TH.
  • TH puede acceder a las funciones y definiciones de "módulo privado", rompiendo completamente la encapsulación en algunos casos.

Luego hay algunos problemas que hacen que las funciones de TH sean menos divertidas de usar como desarrollador de bibliotecas:

  • El código TH no siempre es composable. Digamos que alguien hace un generador para lentes, y la mayoría de las veces, ese generador se estructurará de tal manera que solo puede ser llamado directamente por el "usuario final", y no por otro código TH, por ejemplo, tomando una lista de constructores de tipo para generar lentes como parámetro. Es difícil generar esa lista en código, mientras que el usuario solo tiene que escribir generateLenses [''''Foo, ''''Bar] Lentes generateLenses [''''Foo, ''''Bar] .
  • Los desarrolladores ni siquiera saben que el código TH puede estar compuesto. ¿Sabías que puedes escribir para forM_ [''''Foo, ''''Bar] generateLens ? Q es solo una mónada, por lo que puede usar todas las funciones habituales en ella. Algunas personas no lo saben, y debido a eso, crean múltiples versiones sobrecargadas de esencialmente las mismas funciones con la misma funcionalidad, y estas funciones llevan a un cierto efecto de hinchazón. Además, la mayoría de las personas escriben sus generadores en la mónada Q incluso cuando no tienen que hacerlo, lo cual es como escribir bla :: IO Int; bla = return 3 bla :: 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 un efecto de eso.

Finalmente, hay algunas cosas que hacen que las funciones de TH sean menos divertidas de usar como usuario final:

  • Opacidad. Cuando una función TH tiene el tipo Q Dec , puede generar absolutamente cualquier cosa en el nivel superior de un módulo, y usted no tiene absolutamente ningún control sobre lo que se generará.
  • Monolitismo No puede controlar cuánto genera una función TH a menos que el desarrollador lo permita; si encuentra una función que genera una interfaz de base de datos y una interfaz de serialización JSON, no puede decir "No, solo quiero la interfaz de base de datos, gracias; rodaré mi propia interfaz JSON"
  • Tiempo de ejecución El código TH tarda un tiempo relativamente largo en ejecutarse. El código se interpreta de nuevo cada vez que se compila un archivo y, a menudo, el código TH en ejecución requiere una tonelada de paquetes que deben cargarse. Esto ralentiza considerablemente el tiempo de compilación.