debugging - significado - que es un debug y para que sirve
¿Cómo se logra la depuración en un lenguaje de programación funcional perezoso? (5)
Me gustaría saber cómo se logra la depuración en un lenguaje funcional perezoso.
¿Se pueden utilizar puntos de interrupción, declaraciones impresas y técnicas tradicionales? ¿Es esto incluso una buena idea?
Tengo entendido que la programación funcional pura no permite efectos secundarios, con la excepción de las mónadas.
La orden de ejecución tampoco está garantizada.
¿Tendría que programar una mónada para cada sección del código que desea probar? Me gustaría tener una idea de esta pregunta de alguien con más experiencia en esta área.
De la experiencia con Clojure (que es perezosa, funcional y alienta pero no impone la pureza):
Puede establecer puntos de interrupción al igual que con cualquier otro idioma. Sin embargo, debido a la perezosa evaluación, es posible que estos no se llamen de inmediato, pero se verán afectados tan pronto como se evalúe la estructura perezosa.
En lenguajes funcionales perezosos que permiten efectos secundarios (incluido Clojure), puede insertar impresiones y otros registros de depuración con relativa facilidad. Personalmente me parece muy útil. Debes tener cuidado con el momento en que estos se llaman debido a la pereza, pero si no ves la salida en absoluto, puede ser un indicio de que tu código no se está evaluando debido a la pereza ...
Habiendo dicho todo lo anterior, hasta ahora nunca he necesitado recurrir al depurador. A menudo, unas pocas pruebas simples (tal vez en el REPL) son suficientes para verificar que el código funcional está funcionando correctamente, y si éstas fallan, generalmente es bastante obvio qué es lo que no funciona.
Nada le impide usar puntos de interrupción en un programa funcional evaluado perezosamente. La diferencia con la evaluación impaciente es cuando el programa se detendrá en el punto de interrupción y cómo se verá la traza. El programa se detendrá cuando la expresión en la que se establece un punto de interrupción se esté reduciendo (obviamente).
En lugar del seguimiento de la pila al que estás acostumbrado, obtienes las reducciones que condujeron a la reducción de la expresión con el punto de interrupción.
Pequeño ejemplo tonto. Tienes este programa Haskell.
add_two x = 2 + x
times_two x = 2 * x
foo = times_two (add_two 42)
Y pones un punto de interrupción en la primera línea ( add_two
), luego add_two
foo
. Cuando el programa se detiene en el punto de interrupción, en un lenguaje impaciente esperaría tener una traza como
add_two
foo
y times_two
ni siquiera ha comenzado a evaluarse, pero en el depurador de GHCi se obtiene
-1 : foo (debug.hs:5:17-26)
-2 : times_two (debug.hs:3:14-18)
-3 : times_two (debug.hs:3:0-18)
-4 : foo (debug.hs:5:6-27)
<end of history>
que es la lista de reducciones que condujo a la reducción de la expresión en la que colocó el punto de interrupción. Tenga en cuenta que se parece a times_two
"llamado" foo
aunque no lo haga explícitamente. Puede ver en esto que la evaluación de 2 * x
en times_two
(-2) forzó la evaluación de (add_two 42)
(-1) desde la línea foo
. Desde allí puede realizar un paso como en un depurador imperativo (realizar la siguiente reducción).
Otra diferencia con respecto a la depuración en un lenguaje impaciente es que las variables pueden no estar aún evaluadas. Por ejemplo, en el paso -2 de la traza anterior e inspeccionar x
, encontrará que sigue siendo un thunk sin evaluar (indicado por paréntesis en GHCi).
Para obtener información y ejemplos mucho más detallados (cómo recorrer el rastreo, inspeccionar valores, ...), consulte la sección Depurador de GHCi en el manual de GHC. También está el IDE de Leksah que no he usado todavía como usuario de VIM y terminal, pero tiene una interfaz gráfica para el depurador de GHCi según el manual.
También pidió declaraciones impresas. Solo con funciones puras, esto no es tan fácil como lo haría una declaración de impresión dentro de la mónada IO. Entonces, tienes una función pura.
foo :: Int -> Int
y si desea agregar una declaración de seguimiento, la impresión devolverá una acción en la mónada IO y, por lo tanto, tendría que ajustar la firma de la función en la que desea colocar esa declaración de seguimiento, y las firmas de las funciones que la llaman, ...
Esta no es una buena idea Por lo tanto, necesita alguna forma de romper la pureza para lograr declaraciones de rastreo. En Haskell, esto se puede hacer con unsafePerformIO
. Ahí está el módulo Debug.Trace
que ya tiene una función
trace :: String -> a -> a
que da salida a la cadena y devuelve el segundo parámetro. Sería imposible escribir como una función pura (bueno, si pretendes realmente generar la cadena, eso es). Utiliza unsafePerformIO
bajo el capó. Puede poner eso en una función pura para generar una huella.
¿Tendría que programar una mónada para cada sección del código que desea probar?
Yo sugeriría más bien lo contrario, hacer tantas funciones puras como sea posible (supongo que aquí se refiere a la mónada IO para imprimir, las mónadas no son necesariamente impuras). La evaluación perezosa le permite separar el código IO del código de procesamiento de manera muy limpia.
Si las técnicas de depuración imperativas son una buena idea o no, depende de la situación (como de costumbre). Considero que las pruebas con QuickCheck / SmallCheck son mucho más útiles que las pruebas unitarias en idiomas imperativos, por lo que primero iría por esa ruta para evitar la mayor cantidad de depuración posible. Las propiedades de QuickCheck realmente hacen buenas especificaciones de funciones concisas (una gran cantidad de código de prueba en lenguajes imperativos se parece a otro blob de código para mí).
Un truco para evitar una gran cantidad de depuración es descomponer la función en muchas subfunciones más pequeñas y probar tantas de ellas como sea posible. Esto puede ser un poco desagradable cuando viene de la programación imperativa, pero es un buen hábito sin importar qué idioma esté usando.
Entonces, de nuevo, ¡depurando! = Probando y si algo sale mal en algún lugar, los puntos de interrupción y los rastros pueden ayudarlo.
No creo que este tema pueda tratarse en un espacio corto. Por favor, lea los documentos disponibles en los siguientes enlaces:
Nunca he profundizado en nada terriblemente complicado en Haskell, pero el hecho de que los efectos secundarios hayan desaparecido prácticamente ha eliminado la mayor parte de la necesidad de depuración. Las funciones puras son extremadamente simples de probar y verificar sin un depurador.
Por otro lado, experimenté un par de veces que necesitaba depurar algo dentro de una mónada, en cuyo caso ya podía imprimir / registrar / lo que sea.
Al menos para programas o sistemas más pequeños, la depuración desaparece de la ventana. La escritura fuerte y la comprobación de tipos estática eliminan aún más los errores tradicionales que se encuentran en la programación de procedimientos. La mayoría de los errores, si los hay, son errores lógicos (llamados funciones incorrectas, error matemático, etc.), muy fáciles de probar interactivamente.
Permítame anunciar una herramienta propia para depurar problemas de pereza. Me ayudó a resolver en una hora una pérdida de memoria relacionada con la pereza que ya había pasado 2 días depurando.
http://www.haskell.org/pipermail/haskell-cafe/2012-January/098847.html