Como vimos en los ejercicios de la clase anterior, en muchos casos las funciones que queremos hacer no son casos “puros” de alguna de las tres generalizaciones, sino que para solucionarlas debemos dar un giro de tuerca más. Vamos a ver dos estrategias distintas.
La primera estrategia está basada en la técnica de “divide y vencerás”, también conocida como modularización, en la que dividimos el problema en partes más chicas que solucionamos por separado y finalmente unimos para obtener la solución total.
La segunda estrategia consiste en complejizar las funciones con las que trabajamos en las generalizaciones, especialmente en los acumuladores.
Muchos problemas se pueden enfocar con ambas estrategias, aunque algunos sólo admiten una de ellas.
Veamos la función longitud1), que cuenta cuántos elementos tiene una lista. Podríamos definirla así:
longitud :: [a] -> Int longitud xs = foldr (+) 0 xs
Pero esta función no anda! ¿por qué? porque el operador + requiere que sus argumentos sean números, y la declaración de tipos de longitud no nos garantiza que la lista sea de números. Por lo tanto, habrá que garantizarlo de alguna forma: convirtiendo los elementos de la lista a números. Y además, no sólo queremos convertirlos a números, queremos convertirlos de forma que se cuente 1 por cada elemento de la lista, así que vamos a tener que convertir todos los elementos en 1. Sabemos todo esto, pero cómo lo hacemos?
Para darnos cuenta de qué funciones hay que combinar y cómo hay que hacerlo, podemos pensar intuitivamente en lo que hay que hacer para resolver la tarea que se propone en la función, paso a paso. En primer lugar, habrá que convertir todos los elementos de la lista en 1, en segundo lugar, habrá que sumar todos esos 1 para llegar a un número que será el total de elementos.
Entonces, empecemos por crear una función que toma un elemento cualquiera y devuelve el valor constante 1:
sea1 :: a -> Int sea1 x = 1
Ahora lo que tenemos que hacer es aplicar esta función a nuestra lista, mediante la función que generaliza la aplicación, aplica (o map si usamos la que viene definida en el preámbulo):
convierte1 :: [a] -> [Int] convierte1 xs = map sea1 xs
Una vez tenemos la lista convertida en números, hay que sumarlos, usando un acumulador (la función foldr si usamos la que viene definida en el preámbulo):
longitud :: [a] -> Int longitud xs = foldr (+) 0 (convierte1 xs)
o bien, reemplazando convierte1 por su definición:
longitud xs = foldr (+) 0 (map sea1 xs)
Veamos otro ejemplo: la función cuantosCumplen, que dado un predicado p y una lista xs, nos devuelve un número que indica cuántos elementos de la lista cumplen con p. Como vemos, la forma externa de esta función es un acumulador, ya que toma una lista pero no devuelve una lista sino un valor. Sin embargo, no aplicamos una función a toda la lista sin más, sino sólo a los elementos que cumplen con el predicado, lo cual parece claramente un filtro . Por lo tanto, parece claro que en esta función habrá que combinar un filtro y un acumulador.
Pensemos en lo que hay que hacer para resolver la tarea que se propone en la función, paso a paso. Primero queremos filtrar los elementos que cumplen con el predicado, y luego contar cuántos son. Bien, entonces lo hagamos paso por paso: primero, filtremos los elementos que cumplen con el predicado. Esta función es un filtro, por lo tanto la escribiremos como tal (usando la función filter del preámbulo):
filtraCuantosCumplen :: (a -> Bool) -> [a] -> [a] filtraCuantosCumplen p xs = filter p xs
Esta función filtraCuantosCumplen nos devolverá una lista que contiene únicamente los elementos de la lista original que cumplen con el predicado p. Ahora necesitamos contar cuántos son estos elementos. Para eso podemos aplicar la función longitud que hemos visto arriba, o volver a definirla:
cuenta :: [a] -> Int cuenta xs = foldr (+) 0 (map sea1 xs)
La función cuantosCumplen combinará estas dos funciones para obtener su resultado:
cuantosCumplen :: (a -> Bool) -> [a] -> Int cuantosCumplen p xs = cuenta ( filtraCuantosCumplen p xs )
O también lo podemos escribir así:
cuantosCumplen' :: (a -> Bool) -> [a] -> Int cuantosCumplen' p xs = foldr (+) 0 ( map sea1 (filter p xs) )
Como ven, la estrategia divide y vencerás desmenuza el problema en pasos chiquitos, que tratamos por separado, comprobamos que cada paso hace lo que esperamos que haga, y los combinamos a todos para obtener el resultado final.
Otra forma de enfocar un problema complejo es haciendo más compleja la función que se aplica a la lista. Esta aproximación es más propia del paradigma de programación funcional y es especialmente útil en el caso de los acumuladores. Veamos cómo se trataría de esta forma la función longitud.
En el apartado anterior habíamos visto cómo convertíamos primero la lista en una lista de 1, y luego sumábamos todos los 1. Ahora la misma función que suma nos va a convertir a los elementos en 1. Cómo sería una función así? Recordemos además que debe tener la estructura de tipos que la haga compatible con foldr:
foldr :: (a -> b -> b) -> [a] -> b
Recuerden cómo se aplica la función de foldr, en el paso recursivo:
foldr f n (x:xs) = f x (foldr f n xs)
La función toma dos argumentos: uno que es la cabeza de la lista y otro que es el resultado de aplicar el acumulador al resto de la lista.
Por lo tanto, la función que buscamos deberá tener el tipo (a → b → b), y sumar 1 por cada elemento de la lista, y con la forma que acabamos de ver, sumando 1 por la cabeza al resultado de sumar el resto de la lista:
suma1 :: a -> b -> b suma1 x y = 1 + y
En suma1, x sería el argumento que pasamos como cabeza de la lista y y sería el resultado de sumar el resto de la lista.
Una vez tenemos la función definida, la incorporamos a la función recursiva global:
longitud' :: [a] -> Int longitud' xs = foldr suma1 0 xs
Si desglosamos esta definición, trasladándola a la estructura de foldr, con caso base y caso recursivo, quedaría así:
longitud'' :: [a] -> Int longitud'' [] = 0 longitud'' (x:xs) = 1 + (longitud'' xs)
Pero por supuesto, ahora que ya dominamos las funciones generalizadas (map, filter, foldr), no queremos escribir más casos base y casos recursivos.
Otro buen ejemplo de complejización de la función que aplicamos a la lista es el de la función reversa, que nos da una lista con sus elementos al revés (primero el último, segundo el anteúltimo, etc.). Esta función es un acumulador, aunque nos dé una lista como resultado; esta lista debe verse como un valor, y no como el resultado de una aplicación.
Para ayudar a pensar esta función, que es bastante complicada, vamos a dar primero su definición con caso base y caso recursivo:
reversa :: [a] -> [a] reversa [] = [] reversa (x:xs) = (reversa xs) ++ [x]
De esta forma, los elementos se van colocando al final de la lista: el primero se coloca el último, el segundo el penúltimo, etc. Usamos el operador ++ porque el operador : no se puede usar a la derecha de una lista.
La función que trata este problema deberá hacer lo mismo. Esta función debe tener tipo a → b → b, ya que es la función de un acumulador. Pero sabemos que b es el tipo del resultado, y sabemos que el resultado de reversa es [ a ], así que vamos a concretarlo así:
alacola :: a -> [a] -> [a] alacola x y = y ++ [x]
En esta función, la variable y es una lista, y la variable x es la cabeza de la lista. Ahora que ya tenemos la función, sencillamente, la ponemos en su lugar en el acumulador:
reversa' :: [a] -> [a] reversa' xs = foldr alacola [] xs
Para practicar el tratamiento de casos complejos, recomendamos los ejercicios de la clase anterior, que copiamos en la sección de ejercicios.
La recursión en dos argumentos aplica los mismos principios de la recursión, pero a dos argumentos a la vez. Es decir, en un mismo paso de recursión, vamos a consumir dos estructuras de datos, por ejemplo, dos listas. En cualquiera de los pasos, puede suceder que sea vacía una de las estructuras, la otra, ambas o bien que ambas sean no vacías, por lo tanto, en general hay que tener en cuenta 4 casos.
Veamos como ejemplo la función abrochar:
abrochar :: [a] -> [b] -> [(a,b)] abrochar [] [] = [] abrochar (x:xs) [] = [] abrochar [] (y:ys) = [] abrochar (x:xs) (y:ys) = (x,y) : abrochar xs ys
Como 3 de los 4 casos de abrochar devuelven el mismo resultado, intentamos ponerlos en un mismo patrón, un patrón irrefutable que se aplica siempre que no se puede aplicar el patrón del caso recursivo:
abrochar' :: [a] -> [b] -> [(a,b)] abrochar' (x:xs) (y:ys) = (x,y) : abrochar xs ys abrochar' _ _ = []
La función nesimo, que devuelve el n-ésimo elemento de una lista (comenzando de 0), también se puede definir como recursiva en dos argumentos, donde uno es una lista y el otro un natural:
nesimo :: Int -> [a] -> a nesimo _ [] = error "la lista está vacía" nesimo 0 (x:xs) = x nesimo (n+1) (x:xs) = nesimo n xs
Vemos que para esta función sólo hay que definir 3 casos porque el caso nesimo 0 [] cae dentro del caso nesimo _ []. Vemos también que en el caso recursivo no se devuelve ningún resultado, sino que lo que se hace es recorrer la lista según va indicando el número, reduciendo el número en 1 cada vez que se recorre un elemento de la lista, hasta que se llega al punto indicado por el número, que se habrá convertido en 0, y ahí se devuelve el elemento correspondiente, que será, justamente, el n-ésimo. Notar que esta definición se corresponde con la definición de n-ésimo que vimos en el formalismo.
Veamos ahora una función un poco más compleja: encuentraEstafador. Esta función sirve para encontrar personas que estén cobrando subsidio por desempleo y a la vez trabajando, por ejemplo, en la municipalidad. Para encontrarlos, partimos de dos listas, una con las personas que reciben subsidio de desempleo y otra con los trabajadores de la municipalidad. Queremos es encontrar a todas las personas que estén en las dos listas. Podemos hacerlo de una forma muy poco eficiente:
encuentraEstafador :: [Int] -> [Int] -> [Int] encuentraEstafador [] _ = [] encuentraEstafador (x:xs) ys | existe x ys = x : encuentraEstafador xs ys | otherwise = encuentraEstafador xs ys
Esta versión es poco eficiente porque para cada elemento de la primera lista tenemos que recorrer toda la segunda lista. Sin embargo, si suponemos que las listas están ordenadas, podríamos aprovechar este hecho para recorrerlas a ambas de forma más eficiente.
:set +s
para que imprima el número de pasos de reducción que realizó para cada evaluación y comprobar que encuentraEstafador es menos eficiente que encuentraEstafador'.listadoSubsidio :: [(Int,String,Int)] listadoSubsidio = [(23763956,"Pedrito Rico",7), (36518184,"Daniel Socolof",2), (37643221,"Amaranta Buendía",1)] listadoMuni :: [(String,Int,String)] listadoMuni = [("Rubén Daniele",7632663,"21/12/1957"),("Amanda Buendía",37643221,"04/06/1987"),("Caridad Canelón",41122332,"29/2/2000")]