2010-08-23 20 views
58

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.¿Cómo "depurar" Haskell con printfs?

Desde printf es una acción IO, no tengo que levantar todo mi código Haskell dentro del IO mónada sea capaz de este tipo de depuración? ¿O hay una mejor manera de hacer esto (que realmente no quiero hacerlo a mano si se puede evitar)

También encuentro el rastro función: 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! ¿Puede alguien explicarme el comportamiento de la función de rastreo?

+6

Se debe tener en cuenta que 'trace' solo se utiliza para la depuración, y la comunidad lo rechaza si lo usa para la lógica" real ". – luqui

Respuesta

53

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 apuntó: no es necesario que levante su código en la mónada IO. Se implementa de la siguiente

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

Así que hay detrás de las escenas IO pero unsafePerformIO se utiliza para escapar de ella. 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.

-4

Bueno, como todo Haskell se basa en el principio de la evaluación diferida (por lo que el orden de los cálculos es de hecho no 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 FORMA ADECUADA de programación de Haskell).

+1

Downvoted como ambos sus reclamos son falsos. En llamada por necesidad, el orden de los cálculos está determinado estáticamente. Simplemente se determina no solo por el cuerpo de la función, sino también por el contexto externo en el que se ejecuta la función. No es como las dependencias de datos se determinan en tiempo de ejecución y el flujo de control directo impredecible depende de los valores de tiempo de ejecución. En cuanto al segundo párrafo, hay muchas opciones además de REPL: rastreo (incluidos los inspectores de montón más inteligentes que 'trace'), pruebas, depurador ghci y interfaces gráficas (leksah, etc.). – nponeccop

16

trace simplemente se hace impuro. El objetivo de la mónada IO es preservar la pureza (sin IO no detectado por el sistema de tipos) y definir el orden de ejecución de las declaraciones, que de otro modo quedarían prácticamente indefinidas mediante la evaluación diferida.

Por su propio riesgo, sin embargo, puede, 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ía otras formas para la depuración:

1) La reducción de la necesidad de depurar los valores intermedios

  • Escribir pequeños reutilizables, funciones, claros y genéricos cuya corrección es obvia.
  • Combina las piezas correctas con las piezas correctas más grandes.
  • Escribir tests o probar piezas de forma interactiva

2)

  • utilizar puntos de corte, etc (depuración basada en el compilador)

3)

  • Use m genérico onads. Sin embargo, si su código es monádico, escríbalo independientemente de una mónada concreta. Use type M a = ... en lugar de 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 simplemente insertar Identity a para valores puros.
13

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

  • Registro de valores intermedios, tales como el valor de una subexpresión en particular tiene en cada llamada a 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 suelen coincidir. En Haskell, a menudo no:

  • La grabación de valores intermedios puede cambiar el comportamiento del tiempo de ejecución, forzando la evaluación de los términos que, de lo contrario, 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 lo que desea es mantener un registro de valores intermedios, hay muchas maneras de hacerlo - por ejemplo, en lugar de levantar todo en IO, será suficiente un simple Writer mónada, lo que equivale a hacer funciones devuelve una 2-tupla de su resultado real y un valor de acumulador (una especie de lista, por lo general).

Tampoco es generalmente necesario poner todo en la mónada, sólo las funciones que necesita para escribir en el valor de "log" - por ejemplo, se puede factorizar sólo las subexpresiones que podrían necesitar para hacer conexiones, dejando pura la lógica principal, luego vuelva a ensamblar el cálculo global combinando funciones puras y cálculos de registro de la manera habitual con fmap sy otras cosas. Tenga en cuenta que Writer es una excusa lamentable para una mónada: sin forma de leer desde 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, simplemente mover subexpresiones al nivel superior y probar cosas en el REPL funciona bastante bien.

Si realmente desea inspeccionar el comportamiento en tiempo de ejecución de código puro, sin embargo, por ejemplo, para descubrir por qué una subexpresión diverge, en general, no hay forma de hacerlo desde otro código puro - de hecho, esta 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.

1

trace también tiende a sobrevalorar su argumento de impresión, perdiendo muchos de los beneficios de la pereza en el proceso.

+0

bien, no hagas eso entonces :-) (es decir, solo rastrear lo que sientes con seguridad al forzar) – sclv

0

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

+0

Debido a la evaluación perezosa: en realidad no tienes que esperar hasta que el programa haya terminado. –