Programación 1 - Práctica 3

Anuncio
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
Descargar