Los depuradores (debugger en inglés) son una de las herramientas más útiles para los programadores. En la programación de aplicaciones complejas, a veces no es fácil comprender cómo funciona el programa y cuando falla no se sabe bien qué pasa en la memoria del computador o qué llamado hace que el programa falle o en general por qué falló. En tales condiciones vendría de perlas saber cómo se ejecuta cada operación en el programa, ver qué valor tienen las variables y qué pasa antes y después de una instrucción. Ésto es lo que hacen los depuradores: mostrar paso a paso la ejecución del programa mostrando a qué parte del programa salta la ejecución y qué valores van quedando en la memoria. A continuación voy a describir cómo usar el depurador de DrRacket (sucesor de DrScheme) para estudiantes principiantes o intermedios. Disfrútenlo.

Advertencia

En Scheme y en el paradigma de programación funcional es fundamental comprender cómo se evalúan las expresiones que componen un programa. Por otro lado, cuando se aprende a programar se suele hacer la llamada prueba de escritorio para acostumbrarse a conocer cómo se ejecutará el programa en el PC. El ejercicio de la prueba de escritorio es una práctica que ayuda a desarrollar habilidades para que los programadores sean eficientes y rápidos en dar resultados. Una persona que está acostumbrada a escribir código, ver cómo funciona y corregirlo es una persona que tardará 5 veces más que quien conoce bien cómo se comportará el programa antes de codificarlo, sobre todo porque los errores más difíciles de encontrar son los errores lógicos y en eso el depurador no ayuda tanto.
Antes de continuar, tenga en cuenta que si está aprendiendo a programar, debe asegurarse de que utilizará el depurador sólo cuando haya estructurado el programa de tal manera que en su cabeza, paso a paso obtiene los resultados esperados. No es buena práctica usar el PC para intentar encontrar errores cuando en nuestra cabeza el problema y la solución propuesta no están suficientemente claros.

Principiantes/Intermedios vs Avanzados

En los libros de Scheme como How to design programs del MIT, primero se enfocan en enseñar el paradigma funcional y por lo tanto uno se debe olvidar un poco sobre la memoria y las operaciones sobre la memoria hasta que se domine el lenguaje y el paradigma mismo. Por lo anterior, la depuración en DrRacket es ligeramente distinta cuando se usan las reglas de lenguaje para estudiante principiante o intermedio que cuando se usan las de estudiante avanzado. La gran diferencia es que para los primeros sólo se compara la parte del programa que a continuación será reemplazado por otro valor, mientras que para un estudiante avanzado el depurador va a mostrar también los valores en memoria y las colas de llamados pendientes a ejecutar.

Step: depurador para est. principiantes e intermedios

En la interfaz de DrRacket, encima de la ventana de definiciones y hacia la derecha, aparece un símbolo de un pié llamado Step, eso es una alegoría a que en casi todos los entornos de desarrollo el depurador permite un llamado rápido a la ejecución por pasos (step by step o stepper). Una ejecución por pasos implica que se puede controlar la siguiente instrucción a ejecutar, por ejemplo, en un programa de tres líneas se puede llamar al step y ver qué tiene el computador en su memoria y cpu antes de ejecutar la primera línea, luego ejecutar la primera y ver qué hizo el computador tanto en la memoria como en la CPU y así sucesivamente hasta ejecutar la última línea.

Ejemplo: Examine detalladamente el siguiente programa:

;; area-circulo: número --> número
;; Calcular el área de un círculo de radio r.
(define (area-circulo r) (* pi r r) )
;; área-anillo: número, número --> número
;; Calcular el área de un anillo con radio interior y radio exterior.
(define (area-anillo R r) (- (area-circulo R) (area-circulo r)) )
;; Indique cómo se evalúan las siguientes expresiones:
(define r (* 5 2) )
(define c1 (area-circulo r) )
(area-anillo r (/ r 5))

Antes de continuar, miremos cómo sería la ejecución de cada línea. Las definiciones de las funciones sólo crean los identificadores en la memoria (no ejecutan operaciones reales). Las dos definiciones que se piden evaluar, ambas deben evaluar expresiones internas (* 5 2) y (area-circulo r). Como sabemos, cada línea de Scheme que tenga expresiones interiores con valores concretos (no dentro de definiciones) se deben evaluar, por lo tanto el primer paso en la primera definición es reemplazar la expresión interna por un valor concreto:

(define r (* 5 2) ) --> (define r 10)

Ahora la memoria sabe que la variable r contiene el valor 10.

A continuación la siguiente definición también debe evaluar la expresión anterior, sólo que esta vez es un poco más complicada: (area-circulo r). El primer paso es reemplazar la variable por su valor, luego reemplazar el llamado a la función por el cuerpo de la función, reemplazando a su vez el argumento r por su valor:

(define c1 (area-circulo 10) ) --> (define c1 (* pi 10 10) )

Acá pasa algo importante y es que el valor pi, que no es un argumento también debe ser reemplazado por el valor correspondiente

(define c1 (* #i3.141592653589793 10 10) )

Acá hay que hacer una claración: pi es reemplazado sólo porque ya existe, si Scheme encuentra un identificador que no está definido en alguna otra parte la ejecución para y eso es conocido como error de ejecución.

Ahora la definición sí se puede ejecutar y termina dándole a la variable llamada c1 el valor resultante de evaluar la expresión final: #i314.1592

Finalmente, la evaluación de la última línea del programa debe retornar el área de un anillo, siempre y cuando la definición sea correcta, es decir, el cuerpo de la función corresponda a lo que conocemos como área de un anillo. El llamado debe hallar el área para el caso en que el radio exterior es el valor de r y radio interior es r/5. Pero recordemos lo que significa paso a paso: qué pasa en la memoria y la cpu cada vez que se ejecuta una línea del programa.

El primer paso de la ejecución de la última línea es reemplazar las expresiones internas: r y (/ r 5)

(area-anillo 5 1)

Luego reemplazar el llamado a la función por el cuerpo de su definición reemplazando los parámetros por los valores pasados como argumentos

(- (area-circulo 5) (area-circulo 1))

Con lo que he descrito, ahora habrá que volver a reemplazar las expresiones interiores para obtener valores primitivos (números en éste caso). No lo voy a hacer, sino que ya sabemos qué hará el pc a continuación: reemplaza (area-circulo 5) por la definición de area-circulo reemplazando r por 5, halla el valor final (número) y luego hace lo mismo para (area-circulo 1). Finalmente el último paso de la ejecución sería evaluar la expresión de valores primitivos (- #i78.53981633974483 #i3.141592653589793) dando como resultado el valor #i75.39822368615504

Usando el Stepper

Para poder comprobar todo lo que acabo de escribir, seleccione como lenguaje (esquina inferior izquierda) estudiante principiante o intermedio, escriba el código de definición de las funciones y llamados del punto anterior. Ejecútelo para comprobar que no haya errores de sintaxis o ejecución. Luego oprima Step (ícono de pie en la esquina superior derecha).

Lo que aparece es un poco confuso, pero ya sabiendo lo que hace el PC es más claro reconocer lo que se está mostrando. El PC empieza a leer el programa línea a línea y apenas encuentra una expresión a evaluar, la resalta en color verde claro en la parte izquierda del panel y a la derecha muestra en violeta el valor por el cual reemplazó la expresión, es decir, estamos viendo el primer paso de ejecución.

Los controles indicados por las palabras  <step y step> indican ejecutar un paso antes o uno más, respectivamente. Jump… permite ir a algún punto del programa, bien sea al principio del programa, al final del mismo o un punto particular previamente seleccionado (con el ratón) en la ventana de definiciones. Finalmente, la fracción indica cuántos pasos (reemplazos/evaluaciones) ejecutó el PC para terminar el programa en cuestión, el numerador indica en qué paso del total nos encontramos.

Éste mecanismo se conoce como evaluación paso a paso en otros entornos de desarrollo y se asocia con puntos de rompimiento (break points) en los cuales comienza o termina la evaluación paso a paso, visualización de los valores de las variables en el momento de ejecutar la siguiente instrucción e incluso evaluación de expresiones arbitrarias con los valores que tienen las variables en ese momento.

Conclusiones

Cuando un programa se hace muy complejo para llevar la cuenta de todas las operaciones, es indispensable ayudarnos del PC para ver sus interioridades y comprender por qué está fallando. Con frecuencia sucede que algún paso de la ejecución sucede diferente a como lo hemos previsto, bien sea por un problema de mala previsión nuestra o por interioridades del lenguaje que no conocíamos, pero cualquiera que sea el caso, a veces es muy difícil llegar a las causas sin recurrir a alguna forma de ver qué pasa en un punto particular del programa. La depuración no siempre se hace con depuradores, muchos programadores experimentados saben detectar el segmento del programa en que algo está saliendo mal y deciden imprimir el valor de una variable para ver qué tiene y saber por qué falla la ejecución o imprimir algo que sólo sale cuando el programa pasa por cierta parte del código y así comprueban si el programa está haciendo lo que el programador espera, sin embargo, ésta no es una buena práctica porque esos pedazos de código de depuración se pueden quedar y confundir a los usuarios finales cuando el programa se está mostrando o ya en producción.