29/4/2016 Programación 1 ­ Práctica 3 v.6.4 Programación 1 - Práctica 3 1 Diseñando Programas Un programa bien diseñado es aquel que viene acompañado de una explicación de lo que hace, aquel que explica qué tipo de entradas espera y qué resultados produce. Idealmente, también debería demostrarse que realmente hace lo que se afirma. Todo este trabajo extra es necesario ya que los programadores no construyen programas para ellos mismos. Los programadores escriben programas para que otros programadores puedan entenderlos. La mayoría de los programas son largos, compuestos por colecciones complejas de funciones colaborativas, y nadie puede escribir todas estas funciones en un solo día. Además, es muy frecuente que un programador se una a un proyecto, escriba algo de código y luego de un tiempo se vaya del mismo; así otros programadores deben retomar su labor y continuarla. Otra dificultad es que los clientes cambian de opinión sobre qué problema deben resolver los programadores. Es muy frecuente que el cliente tenga una idea bastante acertada del problema que necesita que se resuelva, pero que omita o se equivoque en ciertos detalles. Peor aún, construcciones lógicas complejas tales como los programas son propensas a los errores humanos. Y sí, los programadores cometen errores... Eventualmente alguien descubre los errores y deben corregirse. Para corregirlos resulta necesario re-leer, entender y corregir programas que datan de hace más de un mes, más de un año o de hace decenas de años! A fin de adquirir buenas prácticas de programación, se propone utilizar una receta que nos indica qué pasos hay que seguir y en qué orden para lograr un buen diseño de un programa. 1.1 La Receta Esta receta nos indica los siguientes pasos: 1. Diseño de datos 2. Signatura y declaración de propósito 3. Ejemplos http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 1/13 29/4/2016 Programación 1 ­ Práctica 3 4. Definición de la función 5. Evaluar el código en los ejemplos 6. Realizar modificaciones en caso que el paso anterior genere errores Veamos más en detalle cada paso. 1. Diseño de Datos La información vive en el mundo real, y es parte del dominio del problema. Para que un programa pueda procesar información, esta debe representarse a través de datos. Asimismo, los datos producidos por un problema, deben poder interpretarse como información. En este paso, definimos la forma de representar la información como datos. 2. Signatura y declaración de propósito La signatura de una función indica qué datos consume (cuántos y de qué clase), y qué datos produce. Ejemplos: ; Number -> Number Signatura para una función que consume un número y produce un número. ; Number String String -> Image Signatura para una función que consume un número y dos strings, y devuelve una imagen. La declaración de propósito consiste en una breve descripción del comportamiento de la función. Cualquier persona que lea el programa, debe entender qué calcula la función sin necesidad de inspeccionar el código. 3. Ejemplos A partir de los dos primeros pasos, debemos estar en condiciones de predecir el comportamiento de la función para algunas entradas. 4. Definición de la función En este paso ya escribimos el código que creemos resuelve el problema planteado. http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 2/13 29/4/2016 Programación 1 ­ Práctica 3 5. Evaluar el código en los ejemplos Una vez que contamos con el código de la función, la ejecutamos en los ejemplos propuestos para verificar que (al menos) funciona para esas entradas cuyas respuestas esperadas conocemos. 6. Realizar modificaciones en caso de errores Si en el paso anterior tenemos diferencias entre las respuestas esperadas y las respuestas obtenidas al ejecutar la función, debemos buscar la fuente del error. Podrían estar mal formulados los ejemplos, podría haber un error en el programa, o ambas cosas a la vez (muy raro que suceda). En general, conviene revisar primero que los ejemplos estén bien. 1.2 Ejemplo de aplicación Para ilustrar los pasos, trabajemos con el siguiente problema: Escribir un programa que convierta una temperatura medida en un termómetro Fahrenheit a una temperatura en Celsius. Veamos cómo aplicar la receta para este ejemplo. 1. Diseño de Datos ; Representamos temperaturas mediante números 2. Signatura y declaración de propósito ; Number -> Number ; recibe una temperatura en Fahrenheit, devuelve su equivalente en Celsius 3. Ejemplos ; entrada: 32, salida: 0 ; entrada: 212, salida: 100 ; entrada: -40, salida: -40 4. Definición de la función (define (far->cel f) (* 5/9 (- f 32))) 5. Evaluar el código en los ejemplos > (far->cel 32) 0 > (far->cel 212) 100 > (far->cel (- 40)) -40 6. Realizar modificaciones en caso de errores http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 3/13 29/4/2016 Programación 1 ­ Práctica 3 Para este ejemplo concreto no es necesario realizar modificaciones ya que las evaluaciones en los ejemplos funcionaron tal como se esperaba. Luego de ejecutar cada uno de los pasos de la receta, tenemos como resultado un programa bien diseñado. El resultado final para este ejemplo concreto es el siguiente: ; Representamos temperaturas mediante números ; far->cel : Number -> Number ; recibe una temperatura en Fahrenheit, devuelve su equivalente en Celsius ; entrada: 32, salida: 0 ; entrada: 212, salida: 100 ; entrada: -40, salida: -40 (define (far->cel f) (* 5/9 (- f 32))) 1.3 Diseñemos funciones simples Los siguientes ejercicios sirven como práctica para internalizar el proceso de diseño. Algunos de estos ejercicios son casi copias de ejercicios ya resueltos en prácticas anteriores, salvo que antes usábamos la palabra "defina" y ahora utilizamos "diseñe". Lo que se espera es que trabajen utilizando la receta para crear sus funciones y las soluciones reflejen todas las etapas del diseño. Ejercicio 1. Diseñe una función distancia-origen, que recibe dos números x e y, devolviendo como resultado la distancia al origen del punto (x,y). Ejercicio 2. Diseñe una función distancia-puntos, que recibe cuatro números x1, y1, x2 e y2 y devuelve la distancia entre los puntos (x1, y1) y (x2, y2). Ejercicio 3. Diseñe la función vol-cubo que recibe la longitud de la arista de un cubo y calcula su volumen. Ejercicio 4. Diseñe la función area-cubo que recibe la longitud de la arista de un cubo y calcula su área. Ejercicio 5. Diseñe la función area-imagen que recibe una imagen y calcula su área. No se preocupe por que funcionen para la cadena vacía. Ejercicio 6. Diseñe la función string-insert, que consume un string y un número i e inserta "-" en la posición i-ésima del string. Ejercicio 7. Diseñe la función string-last, que extrae el último caracter de una cadena no vacía. Ejercicio 8. Diseñe la función string-remove-last, que recibe una cadena y devuelve http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 4/13 29/4/2016 Programación 1 ­ Práctica 3 la misma cadena sin el último caracter. 1.4 Testeando funciones Es fácil testear programas pequeños usando el área de interacción, pero la tarea se complica a medida que aumenta la complejidad del código. El testeo de funciones puede convertirse rápidamente en una tarea pesada para el programador. Mientras más complejo sea el código de un programa, más necesidad tenemos de testearlo para evitar errores. Por lo tanto, resulta útil contar con un mecanismo para automatizar el testeo de programas. DrRacket provee una funcionalidad de testeo a través de la función checkexpect. Recordemos el diseño que obtuvimos de la función far->cel utilizando la receta: ; Representamos temperaturas mediante números ; far->cel : Number -> Number ; recibe una temperatura en Fahrenheit, devuelve su equivalente en Celsius ; entrada: 32, salida: 0 ; entrada: 212, salida: 100 ; entrada: -40, salida: -40 (define (far->cel f) (* 5/9 (- f 32))) Podemos automatizar el testeo de los ejemplos propuestos agregando las siguientes líneas de código en el área de definiciones de DrRacket : (check-expect (far->cel -40) -40) (check-expect (far->cel 32) 0) (check-expect (far->cel 212) 100) Una vez agregadas estas líneas podemos presionar el botón EJECUTAR, y veremos que DrRacket reporta que el programa ha pasado con éxito las tres pruebas. Además de ayudar a automatizar el testeo, la función check-expect provee otra ventaja en caso de que falle alguna prueba. Para ver cómo funciona, cambie alguno de los casos, por ejemplo: (check-expect (far->cel -40) 40) Cuando presione el botón EJECUTAR, verá abrirse una ventana adicional. El texto en esta ventana explicará que uno de los tres casos falla. Para dicho caso se mostrarán: la entrada utilizada para el cálculo (-40), el resultado obtenido al ejecutar la función (-40) y el resultado esperado (40); y un link al texto de dicho caso de prueba. El código check-expect puede ubicarse tanto antes como después de las definiciones que se deseen testear. En el resto del curso, y salvo casos excepcionales, preferiremos usar check-expect en lugar de escribir los casos de test como comentarios. Ejercicio 9. Para cada una de las funciones diseñadas en la sección anterior, agregue http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 5/13 29/4/2016 Programación 1 ­ Práctica 3 las líneas de código necesarias para automatizar los casos de prueba. 1.5 Diseñando con estructuras En la Práctica 2 comenzamos a trabajar con estructuras. La primera estructura que vimos fue posn. Esta estructura sirve para representar un punto en el espacio. El primer campo representa la coordenada x del punto mientras que el segundo representa la coordenada y del mismo. Una estructura posn combina dos valores para formar uno solo. Para crear una estructura de este tipo, utilizamos la operación make-posn que consume dos números y devuelve un posn. Por ejemplo, si queremos representar el punto (3,4) usando esta estructura deberíamos utilizar la siguiente línea de código: (make-posn 3 4) Pensemos ahora en diseñar una nueva versión de la función distancia-origen que computa la distancia de un punto al origen de coordenadas. Recuerde que el origen de coordenadas está representado en el extremo izquierdo superior de su pantalla La imagen clarifica la idea de "distancia", es decir, la longitud del camino más directo desde el punto al origen de coordenadas. Veamos cómo aplicar la receta para este nuevo ejemplo. 1. Diseño de Datos ; Representamos puntos mediante estructuras posn 2. Signatura y declaración de propósito ; distancia-origen : posn -> Number ; recibe un punto en el plano, devuelve su distancia al origen de coordenadas 3. Ejemplos http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 6/13 29/4/2016 Programación 1 ­ Práctica 3 (check-expect (distancia-origen (make-posn 0 2)) 2) (check-expect (distancia-origen (make-posn 3 0)) 3) (check-expect (distancia-origen (make-posn 3 4)) 5) (check-expect (distancia-origen (make-posn 8 6)) 10) (check-expect (distancia-origen (make-posn 5 12)) 13) 4. Definición de la función (define (distancia-origen p) (sqrt (+ (sqr (posn-x p)) (sqr (posn-y p))))) 5. Evaluar el código en los ejemplos. Ejecutando el programa DrRacket se encarga de esta tarea. Observamos que todos los casos de test se evalúan de la forma esperada. 6. Realizar modificaciones en caso de errores Como resultado del paso 5, vemos que no es necesario para este ejemplo realizar modificaciones. El diseño que obtenemos de la función distancia-origen utilizando la receta es el siguiente: ; Representamos puntos mediante estructuras posn ; distancia-origen : posn -> Number ; recibe un punto en el plano, devuelve su distancia al origen de coordenadas (check-expect (distancia-origen (make-posn 0 2)) 2) (check-expect (distancia-origen (make-posn 3 0)) 3) (check-expect (distancia-origen (make-posn 3 4)) 5) (check-expect (distancia-origen (make-posn 8 6)) 10) (check-expect (distancia-origen (make-posn 5 12)) 13) (define (distancia-origen p) (sqrt (+ (sqr (posn-x p)) (sqr (posn-y p))))) Ejercicio 10. La distancia de Manhattan es la longitud de cualquier camino desde un punto al origen que respeta la grilla que forman las calles de la ciudad de Manhattan. Veamos aquí dos ejemplos: http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 7/13 29/4/2016 Programación 1 ­ Práctica 3 El camino representado en la figura izquierda muestra una estrategia "directa", yendo hacia la izquierda tan lejos como se puede y luego tantos pasos para arriba hasta llegar al destino. El camino representado a la derecha representa un "camino aleatorio", yendo algunas cuadras a la izquierda, luego algunas hacia arriba, y así sucesivamente hasta llegar al origen de coordenadas. Pero, pensemos un poco... Realmente importa qué camino se toma? Diseñe la función distancia-manhattan, que mida la distancia de Manhattan entre un posn y el origen. Ejercicio 11. Diseñe una función tiempo->segundos, que tome como entrada una estructura que representa una hora (expresada como horas, minutos y segundos) y produzca como salida la cantidad de segundos que pasaron desde la medianoche. Por ejemplo, si consideramos la hora: 12 horas, 30 minutos y 2 segundos y le aplicamos la función tiempo->segundos, el resultado correcto sería 45002. Deberá, además de diseñar la función, proponer la estructura llamada tiempo. 1.6 Diseñando programas interactivos En esta sección mostraremos cómo diseñar correctamente programas interactivos. Las siguientes líneas describen de manera simplificada y sistemática las funciones principales que componen un programa interactivo: ; Estado: un tipo de dato de su elección ; ya sea un Número, un String, una Imagen o una Estructura ; Ese tipo de dato representa al estado de su programa interactivo ; grafica : ; Estado -> Imagen ; big-bang evalúa (grafica e) para obtener una representación gráfica ; del estado actual e del programa ; manejador-tick : ; Estado -> Estado ; en cada tick del reloj, big-bang evalúa ; (manejador-tick e) siendo e el estado actual del programa ; y obtiene así un nuevo estado http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 8/13 29/4/2016 Programación 1 ­ Práctica 3 ; manejador-tecla : ; Estado String -> Estado ; cada vez que se presiona una tecla, big-bang evalúa ; (manejador-tecla e t) para el estado actual e y ; la tecla identificada por t y obtiene así ; un nuevo estado del programa ; manejador-mouse : ; Estado Número Número String -> Estado ; para cada manipulación del mouse, big-bang evalúa ; (manejador-mouse e x y ev) siendo e el estado actual, ; x e y las coordenadas donde se encontraba el puntero ; al momento de producirse el evento, ev el tipo de evento ; que se produjo, obteniendo así el nuevo estado del programa ; fin? : ; Estado -> Booleano ; luego de producido un evento, big-bang evalúa ; (fin? e) siendo e el estado actual del programa ; el programa termina si (fin? e) evalúa a true Asumiendo que ya conoce cómo trabaja big-bang, nos vamos a concentrar ahora en el problema del diseño de programas interactivos. Consideremos el ejemplo sobre el cual ya trabajó en prácticas anteriores: Diseñe un programa que avance un auto en linea recta sobre una imagen vacía, a razón de 3 píxeles con cada tick del reloj. La receta de diseño de programas interactivos, como la que vimos antes para funciones, es una herramienta para transformar de manera sistemática el enunciado de un problema en un programa. Esta receta para diseñar programas interactivos está compuesta por los siguientes cuatro pasos: 1. Para todas las propiedades que permanecen constantes en el tiempo y son necesarias para graficar el estado, debemos usar constantes. Para introducir constantes usamos definiciones. ; La constante ALTO-FONDO se utiliza para fijar el alto del fondo a utilizar (define ALTO-FONDO 60) ; La constante ANCHO-FONDO se utiliza para fijar el ancho del fondo a utilizar (define ANCHO-FONDO 800) ; la constante AUTO se usa para guardar la imagen del auto (define AUTO ) http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 9/13 29/4/2016 Programación 1 ­ Práctica 3 ; la constante Y-AUTO se usa para guardar la coordenada y donde se ubica el auto (define Y-AUTO (/ ALTO-FONDO 2)) ; la constante FONDO guarda el fondo de la escena (define FONDO (empty-scene ANCHO-FONDO ALTO-FONDO)) ; la constante ESTADO-INI representa el estado inicial y ; ubica el auto tocando el extremo izquierdo de la escena (define ESTADO-INI (/ (image-width AUTO) 2)) ; La constante DELTA se utiliza para fijar la cantidad de píxeles ; que avanza el auto en cada tick (define DELTA 3) 2. Aquellas propiedades que cambian a medida que pasa el tiempo o cuando ocurren ciertos eventos (como el presionar una tecla o el presionar un botón del mouse) son justamente las propiedades que describen el estado actual del programa. La tarea aquí es elegir el tipo de dato más conveniente para representar el estado del programa. El tipo de dato elegido debe ir acompañado de comentarios que ayuden al lector a representar información del dominio del problema como datos en el programa y a interpretar los datos del programa como información del dominio del problema. Elija la forma más simple para representar el estado del programa. Para el ejemplo en cuestión, la distancia del centro del auto al margen izquierdo cambia a medida que avanza el tiempo. Si bien la distancia con respecto al margen derecho también cambia, es evidente que sólo se necesita una sola de esas dos distancias para crear la imagen. Las distancias se miden usando números, por lo tanto, una buena representación sería: ; Estado es un Número ; la interpretación del estado es el número de píxeles entre ; el margen izquierdo y el centro de la imagen del auto 3. Una vez que contamos con una representación adecuada de los datos que componen el estado de nuestro programa, necesitamos pasar al diseño de las funciones que necesita la expresión big-bang, es decir, aquellas funciones que sirven para manejar cada uno de los eventos que queremos capturar. Para comenzar necesitamos una función que conecte un estado con una representación gráfica del mismo, así big-bang puede ir mostrando la secuencia de estados como una secuencia de imágenes: ; grafica Luego necesitamos decidir qué tipos de eventos harán evolucionar el estado del programa. Dependiendo de la decisión que se tome aquí, necesitará definir algunas o todas las funciones que siguen: http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 10/13 29/4/2016 Programación 1 ­ Práctica 3 ; manejador-tick ; manejador-tecla ; manejador-mouse Finalmente, si el enunciado del problema sugiere que el programa debe terminar al alcanzar ciertas propiedades, tendremos que diseñar también la función ; fin? Al iniciar esta sección mostramos las signaturas y los propósitos generales de cada una de estas funciones. Tendrá que reformular estos propósitos para ajustarlos mejor a los cómputos que deben realizar estas funciones en el problema planteado. En resumen, al comenzar a diseñar un programa interactivo ya contamos con un esqueleto que nos brinda la función big-bang. Sin embargo, este esqueleto debe ser tallado ad-hoc para nuestro problema concreto. Volviendo a nuestro ejemplo, veamos qué funciones debemos definir. Si bien bigbang nos obliga a definir la función grafica, debemos analizar, teniendo en cuenta el enunciado del problema, qué manejadores de eventos debemos diseñar. Como el auto debe avanzar de izquierda a derecha a medida que pasa el tiempo, debemos diseñar la función manejador-tick. Para diseñar estas funciones utilizaremos la receta vista al principio de esta práctica para diseñar funciones simples. Obtenemos entonces los siguientes diseños: Note que podemos usar la funcionalidad check-expect con tipos de datos diferentes a números, por ejemplo con imágenes ; grafica : Estado -> Imagen ; ubica el centro de la imagen del auto x píxeles a la derecha ; del margen izquierdo ; de la imagen del FONDO (check-expect (grafica 50) (place-image AUTO 50 Y-AUTO FONDO)) (check-expect (grafica 100) (place-image AUTO 100 Y-AUTO FONDO)) (check-expect (grafica 200) (place-image AUTO 200 Y-AUTO FONDO)) (define (grafica x) (place-image AUTO x Y-AUTO FONDO)) ; manejador-tick : Estado -> Estado ; suma DELTA a x para mover el auto hacia la derecha (check-expect (manejador-tick 20) (+ 20 DELTA)) (check-expect (manejador-tick 78) (+ 78 DELTA)) (check-expect (manejador-tick 99) (+ 99 DELTA)) (define (manejador-tick x) (+ x DELTA)) En este paso obtenemos las signaturas y los propósitos específicos de las funciones que usará big-bang para nuestro programa. http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 11/13 29/4/2016 Programación 1 ­ Práctica 3 4. Finalmente, necesitamos definir la función programa-principal. A diferencia de todas las demás funciones del programa interactivo, la función programaprincipal no necesita ser diseñada. Ni siquiera requiere que ser testeada ya que la única razón de su existencia es proporcionar una manera fácil de ejecutar un programa interactivo desde el área de interacción de DrRacket . ; programa-principal : Estado -> Estado ; lanza el programa desde su estado inicial (define (programa-principal ei) (big-bang ei [to-draw grafica] [on-tick manejador-tick])) Por lo tanto, al lanzar el programa interactivo desde el área de interacción ejecutando: > (programa-principal ESTADO-INI) podrá ver el auto desplazarse hacia la derecha, comenzando desde el extremo izquierdo. El programa terminará cuando cerremos la ventana que abre big-bang. Recuerde que la función big-bang devuelve como resultado el estado actual del programa cuando termina su ejecución. Si juntamos todo, el diseño final de nuestro programa es el siguiente: ; Estado es un Número ; la interpretación del estado es el número de píxeles entre ; el margen izquierdo y el centro de la imagen del auto ; La constante ALTO-FONDO se utiliza para fijar el alto del fondo a utilizar (define ALTO-FONDO 60) ; La constante ANCHO-FONDO se utiliza para fijar el ancho del fondo a utilizar (define ANCHO-FONDO 800) ; la constante AUTO se usa para guardar la imagen del auto (define AUTO ) ; la constante Y-AUTO se usa para guardar la coordenada y donde se ubica el auto (define Y-AUTO (/ ALTO-FONDO 2)) ; la constante FONDO guarda el fondo de la escena (define FONDO (empty-scene ANCHO-FONDO ALTO-FONDO)) ; la constante ESTADO-INI representa el estado inicial y ; ubica el auto tocando el extremo izquierdo de la escena (define ESTADO-INI (/ (image-width AUTO) 2)) http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 12/13 29/4/2016 Programación 1 ­ Práctica 3 ; La constante DELTA se utiliza para fijar la cantidad de píxeles ; que avanza el auto en cada tick (define DELTA 3) ; grafica : Estado -> Imagen ; ubica la imagen del auto x píxeles a la derecha del margen izquierdo ; de la imagen del FONDO (check-expect (grafica 50) (place-image AUTO 50 Y-AUTO FONDO)) (check-expect (grafica 100) (place-image AUTO 100 Y-AUTO FONDO)) (check-expect (grafica 200) (place-image AUTO 200 Y-AUTO FONDO)) (define (grafica x) (place-image AUTO x Y-AUTO FONDO)) ; manejador-tick : Estado -> Estado ; suma DELTA a x para mover el auto hacia la derecha (check-expect (manejador-tick 20) (+ 20 DELTA)) (check-expect (manejador-tick 78) (+ 78 DELTA)) (check-expect (manejador-tick 99) (+ 99 DELTA)) (define (manejador-tick x) (+ x DELTA)) ; programa-principal : Estado -> Estado ; lanza el programa desde su estado inicial (define (programa-principal ei) (big-bang ei [to-draw grafica] [on-tick manejador-tick])) Los nombres de las funciones grafica, manejador-tick, manejador-tecla, manejador-mouse, fin? pueden modificarse. Puede nombrarlas como más le guste, siempre y cuando recuerde escribir estos nombres en las cláusulas de la expresión bigbang. Además, las cláusulas pueden aparecer en cualquier orden, no así el estado inicial, que debe ir en la primer posición. Ejercicio 12. Diseñe los programas interactivos de la Práctica 2 - Primera Parte. http://www.fceia.unr.edu.ar/~iilcc/material/practicas/practica3/practica3.html 13/13