Herramientas de usuario

Herramientas del sitio


introalg:taller09_4

¡Esta es una revisión vieja del documento!


Repaso de Programación Funcional, Divide y Vencerás

Comparación de patrones

ordena (x,y), ordena :: (Int,Int) → (Int,Int) que, dados dos enteros, los ordena de menor a mayor.

ambospositivos x y, ambospositivos :: Int → Int → Bool, que dados dos enteros devuelve True si los dos son positivos.

edad :: (Int, Int, Int) → (Int, Int, Int) → Int que dadas dos fechas indica los años transcurridos entre ellas. Por ejemplo edad (20,10,1968) (30,4,1987) = 18.

Suponer (o comprobar con un módulo o definición local!!!!) que las fechas están siempre bien formadas y que la primera es menor o igual a la segunda.

cabeza xs, cabeza :: [a] –> a, que devuelve el primer elemento de una lista.

cola xs, cola :: [a] –> [a], que devuelve toda la lista menos el primer elemento.

esVaciaOPrimer0 xs, esVaciaOPrimer0 :: [Int] → Bool que dada una lista xs decida si xs es vacía o bien su primer elemento es 0.

segundoEsSegundo (x,y) zs, segundoEsSegundo :: (Int,Int) → [Int] → Bool que dada una tupla de enteros (x,y) y una lista de enteros zs comprueba si el segundo elemento de la tupla es igual al segundo elemento de la lista.

recortaDia xs, recortaDia :: [Char] → [Char] que dada una lista de caracteres xs, comprueba si las letras de la lista forman el nombre de un día de la semana, si es así, si el día no es del fin de semana, devuelven la primera letra solamente, en cambio, si el día es de fin de semana, devuelven el nombre del día completo. Ayuda: fíjense que el resultado siempre debe ser una lista de caracteres!

Modularización

Para hacer un programa que resuelva un problema complejo, lo mejor es analizar bien el problema para dividirlo en partes más chicas. Solucionaremos estas partes más chicas cada una por separado, comprobando que efectivamente cada una hace lo que esperamos que haga. Finalmente, las uniremos para llevar a cabo la tarea compleja que nos planteábamos al principio. A esta estrategia se la conoce como modularización, ya que cada una de las partes en las que se divide el problema se llama módulo o subrutina, pero informalmente también se conoce como “divide y vencerás”, ya que lo que hacemos es parecido a dividir un ejército muy numeroso en partes más pequeñas para vencerlas a cada una por separado.

Imaginemos un elaborado sistema de descuentos en un supermercado. Por ejemplo, imaginemos que el super hace un descuento del 20% en el monto total si hacemos una compra de 257 pesos, de 350 pesos, de 489 pesos o mayor a 1000. En cambio, si hacemos una compra menor a 100 pesos el descuento es del 5%. Si el monto está entre 200 y 500 pesos, pero no es ninguno de los anteriores, el descuento es del 10%, si es mayor a 500 pero menor a 1000, el descuento es del 15%, excepto si se trata de una compra de 768 pesos, de 999 o de 573, que tienen un descuento del 20 por ciento. Además, los descuentos también dan puntos: los del 20% aportan un número de puntos 10 veces mayor al monto total de la compra, los del 15% aportan un número de puntos 5 veces mayor al monto total de la compra, los del 10% aportan el doble de puntos que el monto de la compra, y si la compra es menor a 200 pesos, el número de puntos es igual al monto de la compra. Por último, si compramos con tickets, en un descuento del 20% nos devuelven el 10% del monto total, en un descuento del 15% nos devuelven el 5% y en un descuento del 10% nos devuelven en 2%. La devolución en tickets se devuelve en tickets, no se aplica un descuento al monto total a pagar.

Cómo podemos hacer un programa que nos calcule el descuento, los puntos y el descuento en tickets para un monto dado? Veamos una función que traduzca literalmente lo que dijimos acá arriba.

descuento :: (Float,Bool) -> (Float,Float,Float)
-- argumento: un par que representa el monto (Float) y si tenemos tickets (Bool)
-- resultado: una tresupla (Float,Float,Float) que representa el monto total, 
--            restando el descuento correspondiente, el número de puntos 
--            y el descuento en tickets, respectivamente.

descuento (x,y) | x == 257 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x == 257 && not y = ( x - 0.2*x , 10*x, 0)
                | x == 350 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x == 350 && not y = ( x - 0.2*x , 10*x, 0)
                | x == 489 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x == 489 && not y = ( x - 0.2*x , 10*x, 0)
                | x > 1000 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x > 1000 && not y = ( x - 0.2*x , 10*x, 0)
                | x == 768 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x == 768 && not y = ( x - 0.2*x , 10*x, 0)
                | x == 999 && y     = ( x - 0.2*x , 10*x, 0.1*x)
                | x == 573 && not y = ( x - 0.2*x , 10*x, 0)
                | x > 500 && x <= 1000 && y      = ( x - 0.15*x , 5*x, 0.05*x)
                | x > 500 && x <= 1000 && not y  = ( x - 0.15*x , 5*x, 0)
                | x > 200 && x <= 500  && y      = ( x - 0.1*x , 2*x, 0.02*x)
                | x > 200 && x <= 500  && not y  = ( x - 0.1*x , 2*x, 0)
                | x <= 200 = ( x , x , 0)

Como pueden ver, este código es muy grande, bastante ilegible y difícil de mantener. Qué haríamos, por ejemplo, si ahora el supermercado decidiera reducir el número de puntos que dá a los descuentos del 20%? Aplicando la estrategia “divide y vencerás”, podemos crear algo mucho más compacto, legible y fácil de mantener:

descuento' :: (Float,Bool) -> (Float,Float,Float)
descuento' (x,y) | x == 257 || x == 350 || x == 489 || x == 768 || x == 999 || x == 573 = descuento20 (x,y)
                 | x > 1000 = descuento20 (x,y)
                 | x > 500 && x <= 1000 = descuento15 (x,y)
                 | x > 200 && x <= 500 = descuento10 (x,y)
                 | x <= 200 = descuento0 (x,y)

descuento20 :: (Float,Bool) -> (Float,Float,Float)
descuento20 (x,y) | y == True = ( x - 0.2*x , 10*x, 0.1*x)
                  | y == False = ( x - 0.2*x , 10*x, 0)

descuento15 :: (Float,Bool) -> (Float,Float,Float)
descuento15 (x,y) | y == True = ( x - 0.15*x , 5*x, 0.05*x)
                  | y == False = ( x - 0.15*x , 5*x, 0)

descuento10 :: (Float,Bool) -> (Float,Float,Float)
descuento10 (x,y) | y == True = ( x - 0.1*x , 2*x, 0.02*x)
                  | y == False = ( x - 0.1*x , 2*x, 0)

descuento0 :: (Float,Bool) -> (Float,Float,Float)
descuento0 (x,y) = ( x , x , 0)

Tomemos como ejemplo la función pip:

Suponer el siguiente juego: m jugadores en ronda comienzan a decir los números naturales consecutivamente.
Cuando toca un multiplo de 7 o un número con algún dígito igual a 7, el jugador debe decir pip en lugar del número.

Se pide: encontrar un predicado pip.n, pip : Int → Bool que dado un número 0<=n<10000 devuelva diga cuando el jugador debe decir pip.

Con un complicado juego de mod y div podemos llegar a una solución:

pip :: Int -> Bool
pip n = n `mod` 7 == 0 ||
		n `mod` 10 == 7 || (n `div` 10) `mod` 10 == 7 ||
		(n `div` 100) `mod` 10 == 7 || (n `div` 1000) `mod` 10 == 7
		

Si en cambio dividimos el problema en partes, obtenemos una versión mucho más simple de comprender.

pip' :: Int -> Bool
pip' n = n `mod` 7 == 0 || unidad n == 7 || decena n == 7 || centena n == 7 || unidadDeMil n == 7

unidad, decena, centena, unidadDeMil :: Int -> Int
unidad n = n `mod` 10
decena n = (n `div` 10) `mod` 10
centena n = (n `div` 100) `mod` 10
unidadDeMil n = (n `div` 1000) `mod` 10

Siempre es conveniente ser desconfiado y probamos que ambas funciones sean iguales en el rango [0,9999]

Main> filter pip [0..9999] == filter pip' [0..9999]
True

Y hasta podemos generalizar las funciones unidad, decena, etc. en una que nos dé el dígito n-ésimo de la representación decimal, pero para eso necesitamos usar funciones recursivas.
Eso será tema de la próxima clase, pero veamos de todas maneras la “pinta” de la función.

digitos :: Int -> [Int]
digitos 0 = []
digitos n = n `mod` 10 : (digitos (n `div` 10))

La probamos para ver como opera

Main> digitos 0
[]
Main> digitos 1
[1]
Main> digitos 10
[0,1]
Main> digitos 120
[0,2,1]
Main> digitos 123123123
[3,2,1,3,2,1,3,2,1]

Veamos ahora el ejercicio otorgaBeca (x,y,z), otorgaBeca :: (Int,Int,Int) → String que dada una tresupla con la edad del candidato, su promedio y su ingreso anual, devuelva una recomendación sobre su adecuación para un programa de becas, en tres rangos: “Muy Adecuado”, “Adecuado” y “Poco Adecuado”. Aplicando “divide y vencerás”, usar procedimientos distintos para candidatos menores de 30 años y mayores de 30, para candidatos con un ingreso anual mayor que 15000, entre 15000 y 10000 y menor que 10000, y para candidatos con un promedio mayor a 8, entre 6 y 8, entre 6 y 4 y menor a 4.

Definiciones locales

A veces queremos modularizar pero queremos que los módulos sólo estén disponibles para el programa que estamos haciendo en este momento, y no para todo el resto. Por ejemplo, si queremos definir la función “cola” de forma distinta a como se define normalmente, porque lo necesitamos para ese programa en concreto.

Podemos definir módulos (en el caso de haskell, funciones) de forma que sólo las pueda usar un determinado programa, definiéndolas localmente. Vimos un ejemplo de esto en la clase anterior, para definir la función que calcula el área de un prisma:

area :: Int -> Int -> Int -> Int
area alto ancho fondo = 2*frente + 2*lado + 2*tapa
  where
    frente = alto * ancho
    lado   = alto * fondo 
    tapa   = ancho * fondo

En este ejemplo hemos definido localmente las funciones frente, lado y tapa. Eso quiere decir que si tratamos de usarlas fuera de esta función, el intérprete nos dirá que no las conoce, o aplicará otras funciones con el mismo nombre pero definidas en otro lugar. En cambio, cuando usemos la función area, vamos a usar las definiciones que hemos definido localmente. Las variables definidas en la definición que contiene las definiciones globales siguen manteniendo su referencia en las definiciones locales. En este caso, las funciones frente, lado y tapa usan las variables alto, ancho y fondo como si estuviéramos en la función area, porque de hecho estamos definiendo cosas dentro en la función area.

Para definir funciones locales a otra función, usamos la palabra clave where y el espacio horizontal y vertical: las definiciones locales (y la palabra clave where) tienen que estar como mínimo un espacio más a la derecha que la función dentro de la cual se definen, y no pueden estar separadas por una línea en blanco. Veamos qué pasa cuando no respetamos estas normas de sintaxis? Veamos por ejemplo si no respetamos el espacio horizontal:

area a b c = 2*frente + 2*lado + 2*tapa
   where
frente = a*b
lado = b*c
tapa = a*c

El intérprete va a entender que las funciones frente, lado y tapa son independientes de area, por lo tanto no va a entender que la variable b es la misma que la de la definición de area.

Main> :e
ERROR "1.hs":69 - Undefined variable "b"

Funciones locales vs. globales

Ejercicios

introalg/taller09_4.1240183111.txt.gz · Última modificación: 2018/08/10 03:03 (editor externo)