[[teaching:cc4101:tareas:2022-1:tarea2|<< Volver]] ===== Parte 1. Testing de efectos (2 ptos.) ===== En esta sección van a implementar un mecanismo para hacer tests sobre efectos secundarios, en particular, sobre la impresión de caracteres (//printing//). La necesidad de testear efectos secundarios es muy recurrente en proyectos reales, existiendo distintas técnicas para abordarlo (p.ej. usando [[https://en.wikipedia.org/wiki/Mock_object|mocks]]). En esta ocasión, implementarán una solución que utiliza alcance dinámico para redirigir la salida de impresión hacia una estructura de datos. Asegúrense de haber estudiado la [[teaching:cc4101:tareas:2022-1:tarea2:parte0|introducción a SL y CL]] antes de proceder, en particular, la definición de CL (no usamos SL en esta pregunta). En comparación con lo visto en clases, CL cuenta con una nueva expresión ''{printn }'' que imprime el valor de la expresión en pantalla (usando ''println'' de Racket) y retorna el valor de la expresión. Por ejemplo, ejecutar ''{+ 1 {printn {+ 1 2}}}'' debe imprimir 3, y su valor es 4. * Escriba tests de ''printn'' y observe que no es posible chequear (con la función ''test'') que efectivamente se imprima, ni que los valores impresos sean los esperados. Para poder validar los valores impresos, van a utilizar una estrategia que consiste, en esencia, en redirigir la impresión desde la salida estándar hacia un //log//. Luego, los tests simplemente consisten en corroborar el estado del log. ==== Agregando logs: primer intento ==== En un primer intento, van a agregar logs a través de una nueva estructura de datos y manteniendo un registro global de impresiones. En seguida, actualizarán su función de interpretación para que utilice este nuevo mecanismo. Para esta sección y la siguiente, consideren la siguiente estructura, donde se mantiene tanto el valor de ejecución como el log de impresiones. (deftype Result (result val log)) Por otro lado, para mantener un log les recomendamos utilizar el mecanismo de cajas de Racket ([[https://docs.racket-lang.org/reference/boxes.html|documentación]]). A continuación les proveemos una ilustración de la API de cajas, para hacer crecer una lista: > (define log (box '())) ;; Crea una caja con valor inicial lista vacía > (unbox log) ;; Abre la caja y obtiene su valor guardado '() > (set-box! log (cons "hola" (unbox log))) ;; Modifica el contenido de la caja > (set-box! log (cons "chao" (unbox log))) ;; Modifica el contenido de la caja > (unbox log) '("chao" "hola") Con estos dos elementos, van a reemplazar la función ''println'' utilizada en la interpretación de CL, de tal manera de poder rescatar la información de los valores impresos. A modo de preparación para la siguiente etapa, realice los siguientes pasos: * Defina una nueva función de impresión ''println-g'' que, dado un número, lo agrega a un log global (es decir, usen ''define'' para agregar un identificador global). * Modifique ''interp'' para que use ''println-g'', en vez de ''println''. * Defina una función ''interp-g'', que dada una expresión, retorna un valor de tipo ''Result'' (usando ''interp''). La función debe reiniciar el log global en cada llamada. * Defina tests para verificar que efectivamente es capaz de testear la salida de las impresiones. Llegados a este punto, ¡ya son capaces de testear las impresiones de caracteres! Sin embargo, este enfoque tiene dos problemas importantes: - Ya no se imprime en pantalla m(. - El uso de un valor global no es adecuado en un contexto concurrente (p.ej. si se ejecutasen tests en paralelo, el log resultante sería impredecible e incorrecto). En lo que sigue verán cómo solucionar estos puntos. ==== Segundo intento: alcance dinámico ==== Una solución a los problemas introducidos en la parte anterior es utilizar alcance dinámico. En esta sección los guiaremos en el proceso, para el cual harán uso del mecanismo de [[https://docs.racket-lang.org/guide/parameterize.html|parametrización provisto por Racket]]. Un parámetro en Racket es una "caja", con un contenido inicial, cuyo contenido puede ser redefinido localmente, con alcance dinámico. Además, los parámetros de Racket son thread-safe. Para comenzar, se utiliza la función ''make-parameter'', que permite crear un parámetro. El argumento que se pasa a ''make-parameter'' representa el valor inicial del parámetro. Note que para obtener el valor asociado al parámetro, es necesario "aplicarlo", como si fuera una función sin argumentos. > (define location (make-parameter "here")) > (location) "here" Luego, es posible usar la expresión ''parameterize'' para redefinir el valor del parámetro //con alcance dinámico//. El nuevo valor es visible durante toda la ejecución del cuerpo del ''parametrize''. En primer lugar, se pueden definir nuevos valores para parámetros definidos anteriormente y, en seguida, se define el cuerpo donde se van a ver reflejados estos cambios. La idea es que los nuevos valores quedan acotados a este espacio solamente. Fuera de la ejecución del cuerpo de ''parameterize'', un parámetro mantiene su valor original. > (define (where-am-I?) (string-append "I am " (location))) > (where-am-I?) "I am here" > (parameterize ([location "there"]) (where-am-I?)) ;; solo dentro de la evaluación de este cuerpo se ve el nuevo valor "I am there" > (where-am-I?) ;; fuera de la ejecución del cuerpo del parameterize, location sigue teniendo su valor original "I am here" * (0.5 ptos) Modifique su intérprete para que la interpretación de ''printn'' utilice un parámetro para imprimir. Es decir, el valor inicial del parámetro debe ser la función ''println'' de Racket. * (1 pto) Defina una nueva función de interpretación ''interp-p'', que dada una expresión retorna un valor de tipo ''Result''. La función debe hacer uso de ''interp'', pero manteniendo un log local y redefiniendo el valor del parámetro. El nuevo valor del parámetro debe ser una función que registre impresiones en el log local. * (0.5 ptos) Provea tests para verificar que efectivamente es capaz de corroborar la salida de las impresiones. Ahora sí, ya debiesen ser capaces de interpretar sus expresiones en dos modalidades distintas: el modo normal, donde imprimen en pantalla, y el modo de prueba, donde registran su información en un log, lo que les permite corroborar los valores impresos.