debugger debug debugging haskell trace printf-debugging

debugging - debugger - debug in haskell



¿Cómo "depurar" Haskell con printfs? (6)

Bueno, como todo Haskell se basa en el principio de la evaluación perezosa (de modo que el orden de los cálculos no es de hecho determinista), el uso de printf tiene muy poco sentido.

Si REPL + inspeccionar los valores resultantes realmente no es suficiente para su depuración, envolver todo en IO es la única opción (pero no es LA MANERA CORRECTA de la programación de Haskell).

viniendo de la comunidad Ocaml, estoy tratando de aprender un poco de Haskell. La transición funciona bastante bien, pero estoy un poco confundido con la depuración. Solía ​​poner (muchas) "printf" en mi código ocaml, para inspeccionar algunos valores intermedios, o como indicador para ver dónde falló exactamente el cálculo.

Como printf es una acción IO , ¿tengo que levantar todo mi código haskell dentro de la mónada IO para poder realizar este tipo de depuración? ¿O hay una mejor manera de hacerlo (realmente no quiero hacerlo a mano si se puede evitar)

También encuentro la función de rastreo : http://www.haskell.org/haskellwiki/Debugging#Printf_and_friends que parece exactamente lo que quiero, pero no entiendo su tipo: ¡no hay IO en ninguna parte! ¿Alguien puede explicarme el comportamiento de la función de rastreo?


Por lo que vale, en realidad hay dos tipos de "depuración" en cuestión aquí:

  • Registro de valores intermedios, como el valor que una subexpresión particular tiene en cada llamada en una función recursiva
  • Inspección del comportamiento en tiempo de ejecución de la evaluación de una expresión

En un lenguaje imperativo estricto, estos generalmente coinciden. En Haskell, a menudo no:

  • La grabación de valores intermedios puede cambiar el comportamiento del tiempo de ejecución, como al forzar la evaluación de los términos que de otro modo se descartarían.
  • El proceso real de computación puede diferir dramáticamente de la estructura aparente de una expresión debido a la pereza y las subexpresiones compartidas.

Si solo desea mantener un registro de valores intermedios, existen muchas maneras de hacerlo; por ejemplo, en lugar de elevar todo a IO , bastará con una simple mónada de Writer , lo que equivale a hacer que las funciones den como resultado una tupla de 2 su resultado real y un valor de acumulador (algún tipo de lista, por lo general).

Tampoco suele ser necesario poner todo en la mónada, solo las funciones que necesitan escribir en el valor de "registro"; por ejemplo, puede restar importancia a las subexpresiones que podrían necesitar hacer el registro, dejando pura la lógica principal, luego reensamble el cálculo general combinando funciones puras y cálculos de registro de la manera habitual con fmap s y whatnot. Tenga en cuenta que Writer es una especie de excusa lamentable para una mónada: sin forma de leer el registro, solo escribir en él, cada cálculo es lógicamente independiente de su contexto, lo que hace que sea más fácil hacer malabares con las cosas.

Pero en algunos casos, incluso eso es exagerado: para muchas funciones puras, solo mover subexpresiones al nivel superior y probar cosas en el REPL funciona bastante bien.

Sin embargo, si desea realmente inspeccionar el comportamiento en tiempo de ejecución del código puro, por ejemplo, para descubrir por qué una subexpresión diverge, en general no hay forma de hacerlo a partir de otro código puro , de hecho, esto es esencialmente la definición de pureza Entonces, en ese caso, no tiene más remedio que usar herramientas que existen "fuera" del lenguaje puro: funciones impuras como unsafePerformPrintfDebugging --errr, quiero decir trace --o un entorno de tiempo de ejecución modificado, como el depurador GHCi.


Si puede esperar hasta que el programa finalice antes de estudiar la salida, entonces apilar una mónada Writer es el enfoque clásico para implementar un registrador. Lo uso here para devolver un conjunto de resultados del código HDBC impuro.


trace es el método más fácil de usar para la depuración. No está en IO exactamente por la razón que apuntaste: no es necesario que levantes tu código en la mónada IO . Se implementa así

trace :: String -> a -> a trace string expr = unsafePerformIO $ do putTraceMsg string return expr

De modo que hay IO detrás de escena pero unsafePerformIO se usa para escapar de él. Esa es una función que potencialmente rompe la transparencia referencial que puede adivinar mirando su tipo IO a -> a y también su nombre.


trace simplemente se vuelve impuro. El objetivo de la mónada IO es preservar la pureza (ningún IO desapercibido para el sistema de tipos) y definir el orden de ejecución de los enunciados, que de otro modo quedarían prácticamente indefinidos mediante la evaluación perezosa.

Sin embargo, bajo el riesgo propio, puedes, sin embargo, hackear juntos algunos IO a -> a , es decir, realizar IO impuro. Esto es un truco y, por supuesto, "sufre" de una evaluación perezosa, pero eso es lo que hace el rastreo simplemente por el bien de la depuración.

Sin embargo, sin embargo, probablemente deberías ir por otras vías para la depuración:

1) Reducir la necesidad de depurar valores intermedios

  • Escriba funciones pequeñas, reutilizables, claras y genéricas cuya corrección sea obvia.
  • Combina las piezas correctas con las mejores piezas correctas.
  • Escribir tests o probar piezas de forma interactiva

2)

  • Use puntos de interrupción, etc. (depuración basada en el compilador)

3)

  • Usa mónadas genéricas. Sin embargo, si su código es monádico, escríbalo independientemente de una mónada concreta. Utilice el type M a = ... lugar de IO ... simple IO ... Luego puede combinar fácilmente las mónadas a través de transformadores y poner una mónada de depuración en la parte superior. Incluso si la necesidad de mónadas se ha ido, puede insertar Identity a para valores puros.

trace también tiende a sobre evaluar su argumento para imprimir, perdiendo muchos de los beneficios de la pereza en el proceso.