¿Qué tiene de malo la Plantilla Haskell?

Resuelto Dan Burton asked hace 12 años • 6 respuestas

Parece que la comunidad de Haskell suele considerar a Template Haskell como una conveniencia desafortunada. Es difícil expresar con palabras exactamente lo que he observado a este respecto, pero considere estos pocos ejemplos.

  • Plantilla Haskell incluida en "Lo feo (pero necesario)" en respuesta a la pregunta ¿ Qué extensiones de Haskell (GHC) deberían usar/evitar los usuarios?
  • Plantilla Haskell consideró una solución temporal/inferior en vectores sin caja de valores de nuevo tipo (lista de correo de bibliotecas)
  • A menudo se critica a Yesod por confiar demasiado en Template Haskell (consulte la publicación del blog en respuesta a este sentimiento)

He visto varias publicaciones de blog donde la gente hace cosas bastante interesantes con Template Haskell, permitiendo una sintaxis más bonita que simplemente no sería posible en Haskell normal, así como una tremenda reducción de texto repetitivo. Entonces, ¿por qué se menosprecia a Template Haskell de esta manera? ¿Qué lo hace indeseable? ¿En qué circunstancias se debe evitar Template Haskell y por qué?

Dan Burton avatar Jun 02 '12 03:06 Dan Burton
Aceptado

Una razón para evitar Template Haskell es que, en su conjunto, no es seguro en absoluto, lo que va en contra de gran parte del "espíritu de Haskell". A continuación se muestran algunos ejemplos de esto:

  • No tienes control sobre qué tipo de Haskell AST generará un fragmento de código TH, más allá de donde aparecerá; puedes tener un valor de tipo Exp, pero no sabes 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 un cierto tipo, o solo declaraciones de funciones, o solo patrones de coincidencia de constructores de datos, etc.
  • Puede generar expresiones que no se compilan. ¿Generaste una expresión que hace referencia a una variable libre fooque no existe? Mala suerte, solo verás eso cuando uses tu 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 completamente peligroso:

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

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

  • El código TH no siempre es componible. Digamos que alguien fabrica un generador para lentes y, en la mayoría de los casos, ese generador estará estructurado de tal manera que solo el "usuario final" pueda llamarlo directamente y no mediante otro código TH, por ejemplo tomando una lista de constructores de tipos para generar lentes como parámetro. Es complicado generar esa lista en código, mientras que el usuario sólo tiene que escribir generateLenses [''Foo, ''Bar].
  • Los desarrolladores ni siquiera saben que se puede componer código TH. ¿Sabías que puedes escribir forM_ [''Foo, ''Bar] generateLens? Qes solo una mónada, por lo que puedes usar todas las funciones habituales en ella. Algunas personas no lo saben y, por 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 escriben sus generadores en la Qmónada incluso cuando no es necesario, lo cual es como escribir bla :: IO Int; bla = return 3; le está dando a una función más "entorno" del que necesita, y los clientes de la función deben proporcionar ese entorno como resultado de ello.

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

  • Opacidad. Cuando una función TH tiene tipo Q Dec, puede generar absolutamente cualquier cosa en el nivel superior de un módulo y usted no tiene ningún control sobre lo que se generará.
  • Monolitismo. No puedes 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 la base de datos, gracias; crearé mi propia interfaz JSON".
  • Tiempo de ejecución. El código TH tarda relativamente mucho 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 gran cantidad de paquetes que deben cargarse. Esto ralentiza considerablemente el tiempo de compilación.
dflemstr avatar Jun 01 '2012 20:06 dflemstr

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 cotizaciones funcionan a veces, pero muchas veces hay que realizar injertos y plomería de AST manualmente. La API es grande y difícil de manejar, siempre hay muchos casos que no le interesan pero que aún necesita enviar, y los casos que sí le interesan tienden a estar presentes en múltiples formas similares pero no idénticas (datos vs. nuevo tipo, registro -estilo versus constructores normales, etc.). Es aburrido y repetitivo de 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 un infierno. No poder empalmar funciones definidas en el mismo módulo es la parte más pequeña: 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 todo lo anterior. Otros lenguajes con esta propiedad (C, C++) la hacen viable al permitirle reenviar declaraciones, pero Haskell no lo hace. 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 se pueden formular leyes que las instancias deben obedecer y que los clientes pueden asumir. Si utiliza 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), podrá decir "para tipos de suma, funciona así, para tipos de productos, funciona así". Template Haskell, por otro lado, son solo macros. No es abstracción a nivel de ideas, sino abstracción a nivel de AST, que es mejor, aunque sólo 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 contrasta con varias extensiones de sistema de tipos que, aunque es posible que GHC solo las implemente en este momento, podría imaginar fácilmente que otros compiladores las adoptarían en el futuro y eventualmente se estandarizarían).

  • 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 en 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 usar CPP.

  • Hay un principio general de que se debe utilizar la herramienta adecuada para el trabajo y la más pequeña que sea suficiente, y en esa analogía, Template Haskell es algo como esto . Si hay una manera de hacerlo que no sea Template Haskell, generalmente es preferible.

La ventaja de Template Haskell es que puedes hacer cosas con ella que no podrías hacer de otra manera, y es una gran ventaja. La mayoría de las veces, las cosas para las que se utiliza TH solo podrían realizarse si se implementaran directamente como características del compilador. Es extremadamente beneficioso tener TH porque le permite hacer estas cosas y porque le permite crear prototipos de posibles extensiones del compilador de una manera mucho más liviana y reutilizable (consulte 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 resuelve, 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 el texto estándar, sino eliminando la necesidad de tener el texto estándar.

* Aunque a menudo siento que CPPtiene una mejor relación potencia-peso para los problemas que puede resolver.

EDITAR 23-04-14: Lo que con frecuencia intentaba abordar en lo anterior, y recientemente lo he logrado exactamente, es que existe una distinción importante entre abstracción y deduplicación. La abstracción adecuada a menudo resulta en deduplicación como efecto secundario, y la duplicación es a menudo un signo revelador de una abstracción inadecuada, pero no es por eso que es valiosa. La abstracción adecuada es lo que hace que el código sea correcto, comprensible y mantenible. La deduplicación sólo lo acorta. Template Haskell, como las macros en general, es una herramienta de deduplicación.

glaebhoerl avatar Jun 03 '2012 15:06 glaebhoerl

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

No encuentro que el hecho de que no puedas escribir TH sea tan preocupante. ¿Por qué? Porque incluso si hay un error, aún será tiempo de compilación. No estoy seguro de si esto fortalece mi argumento, pero 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ás una bonita versión impresa del código generado.

Si una expresión TH/cuasi-comilla hace algo tan avanzado que se pueden ocultar rincones complicados, ¿quizás no sea aconsejable?

Rompo bastante esta regla con cuasi-comillas 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 realizar empalmes en las listas por comprensión generalizadas. Sin embargo, creo que hay muchas posibilidades 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 en árboles TH son una aproximación casi perfecta.

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

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

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. La cuestión del cambio de cuándo es algo diferente.

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 complicado 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 posprocesar los resultados de una expresión TH, utilizando su propio código en tiempo de compilación. No sería mucho código filtrar por tipo/nombre de declaración de nivel superior. Diablos, podrías imaginarte escribir una función que haga esto de manera genérica. Para modificar/desmonolitizar cuasiquoters, puede hacer coincidir el patrón en "QuasiQuoter" y extraer las transformaciones utilizadas, o crear una nueva en términos de la anterior.

mgsloan avatar Jun 02 '2012 02:06 mgsloan