====== Consolidando Programación Funcional ======
=====Tipos de datos y comparación de patrones=====
====Tipos====
En la clase anterior vimos que haskell es un lenguaje tipado. La definición de una función suele venir siempre precedida de su //signatura de tipos//, que tiene la siguiente forma:
nombreFunción :: TipoArgumento1 -> TipoArgumento2 ... -> TipoArgumentoN -> TipoResultado
Por ejemplo:
ordena :: (Int,Int) -> (Int,Int)
ambospositivos :: Int -> Int -> Bool
edad :: (Int, Int, Int) -> (Int, Int, Int) -> Int
cabeza :: [a] -> a
cola :: [a] -> [a]
esVaciaOPrimer0 :: [Int] -> Bool
segundoEsSegundo :: (Int,Int) -> [Int] -> Bool
recortaDia :: [Char] -> [Char]
En estos ejemplos hemos visto los tipos más básicos con los que vamos a trabajar en la clase: enteros ''Int'', booleanos ''Bool'', caracteres ''Char'', tuplas ''(a,b,...,n)'' y listas ''[a,b,...]''.
====Patrones====
Vimos también que en haskell se puede expresar muy naturalmente la definición de una función mediante análisis por casos. Estos casos son lo que en haskell se conoce como //patrones//, y se aplican mediante comparación de patrones (en inglés, //pattern matching//). Cada patrón está separado por un símbolo ''='' de la forma como se tiene que calcular el resultado si ese patrón evalúa a ''True''. El esquema general de las definiciones de funciones mediante patrones sería así:
nombreFunción arg1 arg2 arg3 | patron1 = resultado1
| patron2 = resultado2
| patron3 = resultado3
...
Los patrones son expresiones booleanas, que en general involucran a las variables a las que se asigna nombre al definir la función (''arg1, arg2'', ''x, y, xs''). Por ejemplo, podríamos concretar el ejemplo anterior de esta forma.
nombreFunción arg1 arg2 arg3 | arg1 + arg2 == arg3 = arg3
| arg1 - arg2 == arg3 = (-arg3)
| arg1 + arg2 /= arg3 = error "el tercer argumento no es ni la suma ni la resta de los dos primeros"
Para resolver una expresión, el intérprete de haskell funciona de forma muy parecida al de prolog: dada una expresión, el intérprete busca una definición de función con la que pueda unificar, de la misma forma que el intérprete de prolog buscaba una regla con cuya cabeza pudiera unificar la expresión. Luego, si esta definición tiene patrones, el intérprete se fija si esos patrones evalúan a ''True''. Si es así, el intérprete devuelve como resultado lo que hay a la derecha del símbolo ''=''.
Veamos algunos ejemplos de definición de funciones mediante análisis por casos en patrones, y luego veamos cómo funcionan los mecanismos de unificación.
La función ''ordena (x,y)'', con signatura de tipos ''ordena :: (Int,Int) -> (Int,Int)'', toma una tupla de dos enteros y devuelve una tupla con los dos números, ordenados de mayor a menor. Se nos ocurren tres casos posibles con los que tiene que tratar esta función: cuando el primer número es mayor que el segundo, cuando el segundo es mayor que el primero y cuando ambos son iguales. Lo definimos así:
ordena :: (Int,Int) -> (Int,Int)
ordena (x,y) | x < y = (x,y)
| x > y = (y,x)
| x == y = (x,y)
Veamos que hemos hecho una sola definición para la función, en la que hemos asignado nombres de variables a su argumento, llamando ''x'' al primer elemento de la tupla e ''y'' al segundo. Después hemos desglosado los diferentes casos como patrones alternativos para la función.
Dada una expresión como ''ordena (4,3)'' el intérprete busca alguna definición de función con la que pueda unificar. Encuentra que puede unificar con ''ordena (x,y)'', asignando a ''x'' el valor 4 y a ''y'' el valor 3. Con esta asignación de valores a las variables, pasa a evaluar los patrones. El primer patrón, ''x < y'', se convierte en '' 4 < 3'' y evalúa a falso. En cambio, el segundo, ''x > y'', se convierte en ''4 > 3'' y evalúa a ''True'', con lo cual se calcula el resultado como ''(y,x)'', que en este caso será ''(3,4)'', que es el resultado que nos devuelve el intérprete.
Fíjense que habría otras formas de definir la función, más legibles, más cortas y más eficientes, ya que algunos casos se evaluarían a ''True'' en menos pasos:
ordena' :: (Int,Int) -> (Int,Int)
ordena' (x,y) | x <= y = (x,y)
| x > y = (y,x)
Efectivamente, ''ordena''' es más eficiente que ''ordena'' porque en los casos en los que ''x == y'' evalúa a ''True'' en el primer patrón, mientras que en ''ordena'' tiene que evaluar tres patrones para encontrar el que devuelve ''True''.
La función ''ambospositivos x y'', con signatura de tipos ''ambospositivos :: Int -> Int -> Bool'', dados dos enteros devuelve ''True'' si los dos son positivos, se podría escribir de varias formas. La primera es la que uno escribe **sin pensar mucho antes de escribir**:
ambospositivos :: Int -> Int -> Bool
ambospositivos x y | x >= 0 && y >= 0 = True
| x >= 0 && y < 0 = False
| x < 0 && y >= 0 = False
| x < 0 && y < 0 = False
Si nos fijamos un poco, vemos que los tres últimos patrones se pueden colapsar en uno solo, usando un "o" en lugar de "y":
ambospositivos' :: Int -> Int -> Bool
ambospositivos' x y | x >= 0 && y >= 0 = True
| x >= 0 || y < 0 = False
Pero además, vemos que también podemos expresar lo mismo de otra forma: que nos devuelva ''True'' en el primer caso y ''False'' en cualquier otro, usando algún tipo de **patrón irrefutable**. El patrón irrefutable para las alternativas que se introducen con el símbolo ''|'' se escribe con la palabra clave ''otherwise'', que quiere decir //en cualquier otro caso// y siempre se encuentra al final de una lista de alternativas.
ambospositivos'' :: Int -> Int -> Bool
ambospositivos'' x y | x >= 0 && y >= 0 = True
| otherwise = False
Pero si reflexionamos bien bien sobre el asunto, nos damos cuenta de que en realidad, si el resultado que buscamos es un booleano, ya lo teníamos, delante de nuestros ojos: el primer patrón es un booleano que evalúa a ''True'' si los dos enteros son positivos, y ''False'' en cualquier otro caso. Podemos escribir el programa más corto, más legible y más eficiente de la siguiente forma:
ambospositivos''' :: Int -> Int -> Bool
ambospositivos''' x y = x >= 0 && y >= 0
La función ''edad'' es un buen ejemplo de cómo analizar bien el problema, contemplando todos los casos, puede resultar crucial para encontrar la solución adecuada. La función ''edad'', con signatura de tipos ''edad :: (Int, Int, Int) -> (Int, Int, Int) -> Int'', dadas dos fechas indica los años transcurridos entre ellas. Por ejemplo edad (20,10,1968) (30,4,1987) = 18. Es importante darse cuenta de que, por la forma como se organiza el calendario, dada la segunda fecha, nos encontramos con dos situaciones posibles: que el mes y el día de la primera fecha ya hayan pasado o que no hayan pasado todavía. Si ya ha pasaron el mes y el día de la primera fecha, el resultado se calculará simplemente restando los años. Si no pasaron todavía, el resultado será la resta de los años menos uno:
edad :: (Int, Int, Int) -> (Int, Int, Int) -> Int
edad (diaN,mesN,anioN) (diaA,mesA,anioA) | (mesN < mesA) || (mesN == mesA && diaN < diaA) = (anioA - anioN) - 1
| otherwise = anioA - anioN
Las funciones ''cabeza'' y ''cola'' aplican patrones sobre listas. Veremos también una forma distinta de usar los patrones, directamente en el momento en que nombramos la función y sus parámetros, sin usar el símbolo ''|''. Por ejemplo, la función ''cabeza'', que devuelve el primer elemento de una lista, puede definirse así:
cabeza :: [a] -> a
cabeza (x:xs) = x
Pero qué pasa si decimos ''cabeza []''? El intérprete no encuentra patrón para aplicar. Por lo tanto, hay que definir también qué hacer en el caso de encontrarse con una lista vacía. No podemos devolver una lista vacía porque el resultado tiene que ser un elemento, y no podemos devolver un elemento arbitrario (por ejemplo, 0), porque la lista puede ser de cualquier tipo de elementos, y el resultado tiene que ser del mismo tipo que los elementos de la lista. Por eso usamos la variable de tipo ''a'' en la signatura de tipos, para indicar que puede tratarse de cualquier tipo pero que el tipo del resultado tiene que ser el mismo que el de los elementos de la lista. Si el tipo del resultado pudiera ser independiente del tipo de los elementos de la lista, lo escribiríamos así: ''cabeza :: [a] -> b''. Entonces, cómo podemos hacer para resolver el problema de la lista vacía? podemos devolver un error, de la siguiente forma:
cabeza' :: [a] -> a
cabeza' (x:xs) = x
cabeza' [] = error "la lista es vacía y por lo tanto no tiene cabeza!"
Fíjense que para la definición de ''cabeza'' podemos usar el patrón irrefutable, porque hay partes de los patrones que no necesitamos recuperar para calcular el resultado. Así, podemos prescindir de ''xs'':
cabeza'' :: [a] -> a
cabeza'' (x:_) = x
cabeza'' [] = error "la lista es vacía y por lo tanto no tiene cabeza!"
Pero incluso podemos usar el patrón irrefutable en el segundo caso, ya que éste sólo se aplica si el primero falla, y si el primero falla, claramente se tratará de una lista vacía:
cabeza''' :: [a] -> a
cabeza''' (x:_) = x
cabeza''' _ = error "la lista es vacía y por lo tanto no tiene cabeza!"
Pero esta última solución presenta el inconveniente de que resulta más difícil de leer, así que no deberíamos usarla.
En la definición de ''cola'' nos encontramos con un caso muy parecido al de ''cabeza''. Esta función devuelve toda la lista menos el primer elemento, y podría definirse así:
cola :: [a] -> [a]
cola (_:xs) = xs
cola [] = []
Como el tipo del resultado es lista, para el caso de la lista vacía podemos devolver como resultado una lista vacía, o bien un error, como se prefiera.
=====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.
==== ej.1: ''edad''====
Por ejemplo, para la función de la edad tenemos que suponer que las fechas están siempre bien formadas y que la primera es menor o igual a la segunda. Podríamos incluir esas comprobaciones dentro de la función ''edad'', pero quedaría muy largo y difícil de entender. Hacer una función separada que se encargue de eso y llamarla desde la función ''edad'' resulta mucho más elegante, fácil de leer y fácil de modificar. Vendría a ser algo de esta forma:
edad' :: (Int, Int, Int) -> (Int, Int, Int) -> Int
edad' (diaN,mesN,anioN) (diaA,mesA,anioA) | compruebaParametros (diaN,mesN,anioN) (diaA,mesA,anioA) && (mesN < mesA) || (mesN == mesA && diaN < diaA) = ( anioA - anioN ) - 1
| compruebaParametros (diaN,mesN,anioN) (diaA,mesA,anioA) = anioA - anioN
Podemos usar nombres más cortos para que quede toda la función dentro de una línea, pero entonces nos resultará más difícil recordar qué representa cada variable o función:
e :: (Int, Int, Int) -> (Int, Int, Int) -> Int
e (d,m,a) (f,n,b) | c (d,m,a) (f,n,b) && (m < n || (m == n && d < f) = ( b - a ) - 1
| c (d,m,a) (f,n,b) = b - a
Veamos ahora cómo podríamos plantear la función ''compruebaParametros'':
compruebaParametros :: (Int, Int, Int) -> (Int, Int, Int) -> Bool
compruebaParametros (diaN,mesN,anioN) (diaA,mesA,anioA) = diaN > 0 && diaN <= 31 && diaA > 0 && diaA <= 31 && mesN > 0 && mesN <= 12 && mesA > 0 && mesA <= 12 && anioN <= anioA
Pero cuidado! esta función permitiría un día 31 de febrero! cómo podemos hacerla más específica? añadiendo una restricción para el mes de febrero, tanto para la primera fecha como para el mes de la segunda:
compruebaParametros' :: (Int, Int, Int) -> (Int, Int, Int) -> Bool
compruebaParametros' (diaN,mesN,anioN) (diaA,mesA,anioA) | mesN == 2 && mesA == 2 = diaN > 0 && diaN <= 28 && diaA > 0 && diaA <= 28 && anioN <= anioA = True
| mesN == 2 = diaN > 0 && diaN <= 28 && diaA > 0 && diaA <= 31 && mesA > 0 && mesA <= 12 && anioN <= anioA = True
| mesA == 2 = diaN > 0 && diaN <= 31 && diaA > 0 && diaA <= 28 && mesN > 0 && mesN <= 12 && anioN <= anioA = True
| otherwise = diaN > 0 && diaN <= 31 && diaA > 0 && diaN <= 31 && mesN > 0 && mesN <= 12 && mesA > 0 && mesA <= 12 && anioN <= anioA = True
Y qué pasa con los meses que tienen 30 días y no 31? y con los años bisiestos? la función se complica... si queremos que resulte legible y modificable, tendremos que modularizarla. Una opción de modularización sería lo que sigue:
compruebaParametros'' :: (Int, Int, Int) -> (Int, Int, Int) -> Bool
compruebaParametros'' (diaN,mesN,anioN) (diaA,mesA,anioA) | compruebaMesDia (diaN,mesN,anioN) && compruebaMesDia (diaA,mesA,anioA) && compruebaAnterior (diaN,mesN,anioN) (diaA,mesA,anioA)
compruebaMesDia :: (Int,Int,Int) -> Bool
compruebaMesDia (mes,dia,anio) | mes == 2 = dia > 0 && ( ( bisiesto anio && dia <= 29 ) || dia <= 28 )
| mes == 4 || mes == 6 || mes == 9 || mes == 11 = dia > 0 && dia <= 30
| otherwise = dia > 0 && dia <= 31
compruebaAnterior :: (Int, Int, Int) -> (Int, Int, Int) -> Bool
compruebaAnterior (diaN,mesN,anioN) (diaA,mesA,anioA) | anioA == anioN = mesA > mesN || ( mesA == mesN && diaA > diaN )
| anioA > anioN = True
| otherwise = False
Como ven, un problema complejo resulta mucho más fácil de abordar si hacemos el esfuerzo inicial de analizarlo con detenimiento, detectar cuáles son sus subpartes y las relaciones entre ellas, y programar en función de ese análisis.
==== ej.2: supermercado ====
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)
==== ej.3: ''pip''====
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'' (resto de la división entera) y ''div'' (división entera) 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
=====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 "''compruebaParametros''" de forma distinta para cada función.
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"
===== Ejercicios =====
Si no lo hicieron el día anterior, traten de resolver los siguientes problemas:
- ''esVaciaOPrimer0 :: [Int] -> Bool'' que dada una lista decida si es vacía o bien su primer elemento es 0.
- ''segundoEsSegundo :: (Int,Int) -> [Int] -> Bool'' que dada una tupla de enteros y una lista de enteros comprueba si el segundo elemento de la tupla es igual al segundo elemento de la lista.
- ''recortaDia :: [Char] -> [Char]'' que dada una lista de caracteres, 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
- Definan, según su propio criterio para otorgar becas, la función ''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. Apliquen modularización.
Nuevos problemas:
- Definir una función ''averaver'', con signatura de tipos ''averaver :: (Int,Int,Int) -> (Int,Int,Int)'' que, dada una tresupla de enteros, si todos son positivos, invierte su orden, si todos son negativos, los cambia a positivos, y en cualquier otro caso, devuelve la misma tresupla. Ayuda: usar //otherwise//.
- Definir la función ''ordenaSiPositivos'', con signatura de tipos ''ordenaSiPositivos :: (Int,Int) -> (Int,Int)'', que ordena los números de una tupla de enteros si ambos son positivos, pero si son negativos los deja sin ordenar. Ayuda: reusen funciones que ya han definido antes!!
Viejos problemas, nuevas soluciones: re-piensen los problemas difíciles que planteamos en la segunda clase de prolog usando modularización:
- ¿Cómo sería un programa para recomendar amigos en una red social tipo //facebook//?
- Tenemos una lista de las conexiones por tren entre pares de ciudades, por ejemplo ''conectadas(Tarragona,Barcelona). ''. ¿Cómo sería un programa que nos ayudara a saber si podemos llegar de una ciudad a otra, es decir, si existe una lista de conexiones que nos lleve de una ciudad a otra, o bien eso es imposible? Primero, traten de hacer un programa que nos diga si podemos llegar de una ciudad a otra directamente o bien en dos o en tres pasos...