Programaci´
on Funcional en Haskell
Paradigmas de Lenguajes de Programaci´on1◦ cuatrimestre 2006
1.
Expresiones, valores y tipos
Un programa en lenguaje funcional consiste en definir expresiones que computan (o denotan) valores. As´ı como los valores, en el mundo “real” o “matem´atico”, pertenecen a un conjunto, las expresiones pertenecen a un tipo.
Veamos qu´e tipos pueden tener las expresiones de Haskell: Tipos b´asicos comoInt,Char,Bool, etc.
Funciones , comoa →Int,Bool →(Bool →Bool), etc.
Tuplas de cualquier longitud. Por ejemplo, (2 ∗5 +1, 4 >0) es de tipo
(Int, Bool).
Listas , secuencias ordenadas de elementos de un mismo tipo, con repeti-ciones. [Int] representa el tipo lista de enteros, [Bool] es una lista de booleanos, etc. Las expresiones de tipo lista se construyen con []
(que representa la lista vac´ıa) y: (a:as es la lista que empieza con el elemento a y sigue con la lista as). Tambi´en pueden escribirse entre corchetes, con los elementos separados por comas:
[] :: [Bool] [3] :: [Int]
’a’ : (’b’ : (’c’ : [])) :: [Char] [2 > 0, False, ’a’ == ’b’] :: [Bool] [[], [1], [1,2]] :: [[Int]]
El tipoStringes sin´onimo de[Char], y las listas de este tipo se pueden escribir entre comillas:"plp" es lo mismo que[’p’, ’l’, ’p’].
Tipos definidos por el usuario , con la cl´ausula data. Los valores aso-ciados a estos tipos consisten de un constructor (que se escribe con may´uscula) acompa˜nado de 0 o m´as argumentos.
Este tipo tiene cinco constructores, todos sin argumentos. A esta clase de tipos se los llamaenumerados.
data Either a b= Left a | Right b data Maybe a =Nothing | Just a
Los tipos pueden tener argumentos, lo que los convierte en tipos para-m´etricos. Tipos como los de arriba suelen llamarse sumas o uniones, porque pueden representar la uni´on de varios tipos. En particular,
Either representa la uni´on de dos tipos cualesquiera, y Maybe
repre-senta el mismo conjunto que su argumento, m´as un valor:Nothing.
Left True :: Either True a Just 3 :: Maybe Int
data BinTree a= Nil | Branch a (BinTree a) (BinTree a)
Ac´a vemos que algunos de los constructores pueden tener como argu-mento el mismo tipo que determinan. Tipos as´ı se suelen llamar tipos recursivos. En este caso, BinTree a representa el tipo de los ´arboles binarios cuyos nodos tienen un elemento dea.
Nil :: BinTree a Branch True Nil
(Branch (4 > 0) Nil
Nil) :: BinTree Int
Las funciones sobre tipos construidos con la cl´ausuladata pueden de-finirse porpattern matching. Un patr´on consiste de un constructor con tantas variables como argumentos tenga; al evaluar la funci´on en un argumento, se intenta establecer una correspondencia entre ´el y cada patr´on, reduciendo en la primera ecuaci´on donde se la encuentre.
proximo :: Dia → Dia proximo Lunes =Martes proximo Martes= Miercoles proximo Miercoles = Jueves etc.
aInt :: (Either Bool Int) → Int aInt (Left x) =if x then 1 else 0 aInt (Right x)= x
esVacio :: BinTree a b → Bool esVacio Nil =True
esVacio (Branch _ _ _)= False
Cuando las variables no se usan en el lado derecho de la ecuaci´on, se pueden reemplazar por un_.
Los tipos que permiten acceder a sus constructores y hacer pattern matching se llaman tipos algebraicos. ¡Los booleanos, las tuplas y las listas tambi´en son tipos algebraicos!
fst :: (a, b) → a fst (x, y) = x
length :: [a] → Int length []= []
length (x:xs) =1 + length xs
2.
Currificaci´
on y evaluaci´
on parcial
Currificaci´on es una correspondencia entre:funciones que reciben m´ultiples argumentos y devuelven un resultado
suma :: (Int, Int) → Int suma (x, y) =x + y
funciones que reciben un argumento y devuelven una funci´on interme-dia que completa el trabajo
suma :: Int → Int → Int suma x y= x +y
En este ejemplo,suma x es una funci´on que dadoydevuelve x+y.
Esta correspondencia siempre existe, y en el segundo caso decimos que las funciones est´an currificadas. La ventaja de las funciones currificadas es que permiten la aplicaci´on parcial. ¡En una sola l´ınea estamos definiendo varias funciones!
sucesor :: Int → Int sucesor =suma 1
3.
Polimorfismo y overloading
El sistema de tipos de Haskell permite definir funciones para ser usadas con m´as de un tipo. Ya vimos algunos ejemplos:esVacio, fstylength son funciones polim´orficas. Otras funciones polim´orficas ´utiles son:
flip :: (a → b → c) → (b → a → c) flip f x y = f y x
(.) :: (a → b) → (c → a) → (c → b) (.) f g x= f (g x)
Las funciones polim´orficas en general se definen seg´un la estructura de sus argumentos, sin fijarse en qu´e valores tienen internamente. Por ejem-plo, la longitud de una lista puede calcularse sin saber nada acerca de sus elementos. Veamos ahora este otro ejemplo.
ejemplo 1: Definamos una funci´on que devuelva verdadero cuando todos los elementos de una lista son iguales:
todosIguales []= True todosIguales [x]= True
todosIguales (x:y:xs)= (x == y) && todosIguales (y:xs)
¿Qu´e tipo tiene esta funci´on? En principio, vemos que puede tomar lis-tas de distintos tipos: todosIguales [1,2,3], todosIguales [True, True],
todosIguales "hola"parecen expresiones v´alidas. Sin embargo, por ejemplo,
todosIguales [sucesor, suma 1]no se podr´ıa evaluar, porque las funciones
no pueden compararse por igualdad.
Lo que necesitamos es describir el conjunto de tipos que tienen la ope-raci´on ==, o m´as en general, los tipos que tienen ciertas operaciones en particular. Para ello, Haskell provee lasclases de tipos. En este caso, los que pueden compararse por igualdad corresponden a la claseEq.
todosIguales :: Eq a ⇒ [a] → Bool
? Otras clases ´utiles son:
Show: la clase de los tipos que pueden mostrarse por pantalla
Ord: la clase de los tipos que pueden compararse (por menor, igual, etc.)
Num: la clase de los tipos con operaciones aritm´eticas.
El mecanismo de clases se denominaoverloading. Notemos que==no es una funci´on polim´orfica, por m´as que pueda tomar argumentos de distintos tipos. Una funci´on polim´orfica tienela misma definici´onpara cualquier tipo, y como dijimos, no podr´a explotar “caracter´ısticas particulares” de cada uno. En cambio, una funci´on sobrecargada, entre los distintos tipos, s´olo comparte el nombre (y la aridad): su definici´on puede ser distinta para cada uno de ellos.
4.
Alto orden
En Haskell, las funciones son valores como cualquier otro: Pueden ser argumentos de una funci´on
Pueden almacenarse en estructuras de datos
ejemplo 2: Definamos una funci´on que toma el m´aximo de una lista:
maximo :: Ord a ⇒ [a] → a maximo [x] =x
maximo (x:y:xs) =if x >y
then maximo (x:xs) else maximo (y:xs)
? Esta funci´on es ´util siempre y cuando no nos interese otro orden que el del operador>.
maximo [1,4,3] =4
maximo ["abc", "a", "b"] = "b" maximo [False, True] = True
ejemplo 3: Ahora supongamos que quiero elegir, entre varias secuen-cias, la de mayor longitud.
maxLongitud :: [[a]] → [a] maxLongitud [xs] =xs
maxLongitud (xs:ys:xss) =if length xs > length ys then maxLongitud (xs:xss) else maxLongitud (ys:xss)
? Esta funci´on se parece mucho a la primera, y sin embargo, tuvimos que definirla aparte. ¿Podremos generalizarmaximopara que nos sirva en ambos casos? S´ı: en lugar de tener (>) embebido en la definici´on de la funci´on,
¡tomemos una funci´on de comparaci´on como primer argumento! ejemplo 4:
mejorSegun :: (a → a → Bool) → [a] → a mejorSegun _ [x] =x
mejorSegun comp (x:y:xs) = if comp x y
then mejorSegun comp (x:xs) else mejorSegun comp (y:xs)
maximo = mejorSegun (>)
maxLongitud = mejorSegun (λxs ys → length xs> length ys)
Y podemos definir m´as:
minimo :: Ord a ⇒ [a] → a minimo = mejorSegun (<)
maxElemento :: Ord a ⇒ [[a]] → [a] maxElemento = mejorSegun tieneMaxElemento
? En este ejemplo mostramos varias formas de escribir funciones como argumentos de otras:
Por su nombre, cuando la funci´on est´a definida aparte:length
Por secci´on de operadores: (>),(∗2), etc.
Como funciones an´onimas:(λxs ys →length xs >length ys)
Con cl´ausulas where:
where tieneMaximoElemento xs ys =maximo xs >maximo ys
5.
Listas
Las listas son una construcci´on muy ´util en Haskell. Cuando un programa involucra una secuencia de valores, las listas suelen ayudar a expresarlo de una forma simple y clara.
Hasta ahora vimos c´omo escribir listas a partir de sus constructores, o de darlas expl´ıcitamente. Ac´a vamos a ver otras formas ´utiles de hacerlo.
5.1. Algunas funciones ´utiles sobre listas
take n xsdevuelve losnprimeros elementos de xs
drop n xsdevuelve el resultado de sacarle axslos primerosnelementos
head xsdevuelve el primer elemento de la lista
tail xsdevuelve toda la lista menos el primer elemento
last xsdevuelve el ´ultimo elemento de la lista
init xsdevuelve toda la lista menos el ´ultimo elemento
xs ++ys concatena ambas listas
xs !! ndevuelve eln-´esimo elemento dexs
elem x xsdice sixes un elemento dexs
5.2. Secuencias aritm´eticas
Las siguientes expresiones representan listas de n´umeros en progresi´on aritm´etica:
[1..4] =[1,2,3,4]
[5,7..13]= [5,7,9,11,13] [1..]
De estas, las dos ´ultimas representan listas infinitas. Como tales, por supuesto no tienen un valor asociado, pero pueden usarse para definir otras expresiones1:
take 10 [1..] =[1,2,3,4,5,6,7,8,9,10]
Claramente las secuencias aritm´eticas no son el ´unico mecanismo para definir listas infinitas:
infinitosUnos :: [Int]
infinitosUnos =1 : infinitosUnos
ejemplo 5: ¿C´omo computar el factorial de un n´umero?
factorial :: Int → Int factorial 0 = 1 factorial n = n ∗ factorial (n-1) factorial n = if n == 0 then 1 else n ∗ factorial (n-1) factorial n = product [1..n]
Como vemos, el uso de listas nos da un c´odigo m´as sencillo y nos ahorra la necesidad de escribir la recursi´on expl´ıcitamente. ?
5.3. Listas por comprensi´on
Las listas definidas por comprensi´on tienen la forma
[expresion |selectores, condiciones]
donde un selector es de la formavar ←lista y una condici´on es una expre-si´on booleana. Tanto la expresi´on como las condiciones pueden depender de las variables de los selectores.
[(x,y) | x ← [1,2], y ← [4,5]] =[(1,4),(1,5),(2,4),(2,5)] [(x,y) | x ← [1,3], y ← [1..x]] =[(1,1), (2,1), (2,2),
(3,1), (3,2), (3,3)]
[(x,y) | x ← [1,2], y ← [1..3], y> x] =[(1,2), (1,3), (2,3)]
1
Esto funciona bien porque Haskell utiliza evaluaci´onlazy, que est´a emparentada con elorden normal de reducci´on: cuando una expresi´on puede, como la de arriba, reducirse de m´as de una forma, se elige la expresi´on m´as externa. En el ejemplo presentado, se pod´ıa reducirtake 10 [1..]o solamente[1..], y esto ´ultimo no hubiera terminado. Intuitivamente, la estrategia lazy eval´ua los argumentos de las funciones s´olo en la medida que es necesario. Entonces, en este caso, de la lista[1..] s´olo hace falta computar los primeros diez elementos.
La estrategia de evaluaci´oneager, en cambio, est´a asociada al orden de reducci´on es-tricto: ante m´as de una opci´on, se reducen las expresiones m´as internas, con lo cual, los argumentos de las funciones se eval´uan completamente antes de computarlas.
ejemplo 6: Usando listas por comprensi´on, podemos ordenar una lista con el algoritmo quicksort de una manera clara y concisa:
quicksort [] = []
quicksort (x:xs) =quicksort [y | y ← xs, y ≤ x]
++ [x]++
quicksort [y | y ← xs, y > x]
? ejemplo 7: Para decidir si un n´umero es primo, en lugar de contar sus divisores con recursi´on expl´ıcita, basta con tomar la longitud de una lista:
esPrimo n =length [x | x ← [1..n], n rem x== 0]== 2
?
6.
Esquemas de funciones
6.1. Para listas
ejemplo 8: Definamos una funci´on que duplique los elementos de una lista de enteros.
duplicar :: [Int] → [Int] duplicar [] = []
duplicar (x:xs) =2∗x : duplicar xs
duplicar xs = [2 ∗ x | x ← xs]
Definamos tambi´en una funci´on que, dada una lista de cadenas, devuelva una lista con sus longitudes.
longitudes :: [[a]] → [Int] longitudes [] = []
longitudes (xs:xss) = length xs : longitudes xss
longitudes xss =[length xs | xs ← xss]
Claramente estos esquemas son muy parecidos: lo ´unico que cambia entre uno y otro es la funci´on aplicada en el paso recursivo. Entonces, como ya hemos hecho, podemos generalizarlos en una funci´on de alto orden:
map :: (a → b) → [a] → [b] map f [] =[]
map f (x:xs) = f x : map f xs
map f xs =[f x | x ← xs]
duplicar =map (∗2)
? ejemplo 9: Definamos una funci´on que, dada una lista de enteros, devuelva los que son pares:
pares :: [Int] → [Int] pares [] =[]
pares (x:xs) = if (rem x 2 == 0) then x : pares xs else pares xs
pares xs =[x | x ← xs, rem x 2 == 0]
Y ahora otra que, dada una lista de cadenas y un n´umero, devuelva una con las de mayor longitud que ese n´umero:
masLargasQue :: Int → [[a]] → [[a]] masLargasQue _ [] =[]
masLargasQue n (xs:xss) =if (length xs > n)
then xs : masLargasQue n xss else masLargasQue n xss
masLargasQue n xs = [x | x ← xs, length x >n]
¡La ´unica diferencia entre ellas es el primer argumento de if! ¿C´omo podemos generalizarlas?
filter :: (a → Bool) → [a] → [a] filter _ [] = []
filter p (x:xs) =if p x
then x : filter p xs else filter p xs
filter p xs = [x | x ← xs, p x]
pares = filter (λx → rem x 2 == 0)
= filter ((== 0) . (‘rem‘ 2))
= filter ((== 0) . (flip rem 2))
masLargasQue n =filter ((> n) . length)
? ejemplo 10: Definamos ahora funciones para sumar los elementos de una lista, para multiplicarlos, para contarlos y para concatenarlos.
sum :: Num a ⇒ [a] → a sum [] = 0
sum (x:xs) =x +sum xs
product :: Num a ⇒ [a] → a product [] =1
length :: [a] → Int length [] =0
length (x:xs) = 1+ length xs
concat :: [[a]] → [a] concat [] =[]
concat (xs:xss) =xs ++ concat xss
Nuevamente tenemos un esquema que se repite en las tres funciones. En este caso, las diferencias est´an en el valor devuelto en el caso base y en la funci´on aplicada en el caso recursivo. As´ı que vamos a abstraerlas para crear un esquema general. foldr :: (a → b → b) → b → [a] → b foldr f z [] =z foldr f z (x:xs) =f x (foldr f z xs) sum =foldr (+) 0 product = foldr (∗) 1 length = foldr (λx n → 1 + n) 0 concat = foldr (++) [] ? El esquemafoldr sirve para recorrer una lista “de derecha a izquierda”:
foldr op b (a1 : (a2 : (a3 : []))) =
a1 ‘op‘ (a2 ‘op‘ (a3 ‘op‘ b ))
Notemos ac´a como :se “reemplaza” por opy[]por b.
ejemplo 11: ¿Qu´e computan las siguientes funciones?
f1 :: [Bool] → Bool f1 =foldr (&&) True
f2 :: [a] → [a] f2 =foldr (:) []
f3 :: [a] → [a] → [a] f3 xs ys =foldr (:) ys xs
? As´ı como con foldr se asocia a derecha, podemos escribir un operador gen´erico de recursi´on que asocie a izquierda.
foldl :: (b → a → b) → b → [a] → b foldl f b [] =b
foldl f b (x:xs)= foldl f (f b x) xs
sum’ = foldl (+) 0
sum’ (a1 : (a2 : (a3 : []))) =
foldl (+) ((0 + a1)+ a2) (a3 : []) =
foldl (+) (((0 +a1) + a2)+ a3) [] = ((0+ a1)+ a2) + a3
ejemplo 12: ¿Qu´e computa las siguente funci´on?
f4 :: [a] → [a]
f4 =foldl (flip (:)) []
? Cuando el caso base est´a en una lista unitaria en lugar de en una vac´ıa, se pueden usarfoldr1yfoldl1.
foldr1 :: (a → a → a) → [a] → a foldr1 f (x:xs)= foldr f x xs
foldl1 :: (a → a → a) → [a] → a foldl1 f (x:xs)= foldl f x xs
maximo =foldr1 max
Estos esquemas de recursi´on asocian a las listas un recorrido “est´andar”, a partir del cual se puede definir un conjunto importante de operaciones. To-das ellas se pueden definir entonces sin pattern matching, concentr´andonos ´
unicamente en el aspecto de cada una que las diferencia de las dem´as. ejemplo 13: Definamosmapyfilter usando foldr
map :: (a → b) → [a] → [b] map f = foldr fun []
where fun x xs= f x : xs
map f = foldr (λx xs → f x : xs)
filter :: (a → Bool) → [a] → [a] filter p =foldr selec []
where selec x xs= if p x then x : xs else xs
?
6.2. Para otros tipos algebraicos
Los esquemas generales de recursi´on pueden escribirse para cualquier tipo, y son muy ´utiles para evitar la repetici´on de c´odigo por pattern mat-ching. En general, necesitamos:
Para cada constructor base A a1 ... an del tipo, una funci´on base
z :: a1 →... →an →b.
Para cada constructor recursivo, una funci´on que tome, adem´as de los argumentos no recursivos, los resultados acumulados, y devuelva un nuevo resultado acumulado.
Recordemos la definici´on deBinTreeal principio:
data BinTree a =Nil | Branch a (BinTree a) (BinTree a)
ejemplo 14: Empecemos por definir una funci´on sobre BinTree Int,
que multiplique los nodos del ´arbol, y otra que cuente los elementos:
prodTree :: BinTree Int → Int prodTree Nil = 1
prodTree (Branch x t1 t2) = x ∗ prodTree t1 ∗ prodTree t2
countTree :: BinTree a → Int countTree Nil = 0
countTree (Branch x t1 t2) = 1+ countTree t1 + countTree t2
? Ac´a, al igual que en las listas, hay un ´unico caso base sin argumentos. Pero a diferencia de ellas, el caso recursivo tiene tres, dos de los cuales se corresponden con llamados recursivos propiamente dichos. Para definir
foldTree, necesitaremos entonces una funci´on fde tres argumentos:
foldTree :: (a → b → b → b) → b → BinTree a → b foldTree f z Nil = z
foldTree f z (Branch x t1 t2)= f x (foldTree f z t1) (foldTree f z t2)
prodTree= foldTree (λx y z → x ∗ y ∗ z) 1
countTree= foldTree (λx y z → 1 +y + z) 0
ejemplo 15: ¿C´omo podemos definir la funci´on que dado un ´arbol, devuelva su sim´etrico?
simetrico :: BinTree a → BinTree a simetrico =foldTree rev Nil
where rev x t1 t2 =Branch x t2 t1
?
Referencias
[1] P´agina de Haskellwww.haskell.org
[2] A tour of the Haskell Prelude, describe y da ejemplos de las funciones de uso m´as com´un
http://www.cs.uu.nl/%7Eafie/haskell/tourofprelude.html
[3] Haskell report es la especificaci´on completa y oficial del lenguaje.
[4] A tour of the Haskell Syntax, una descripci´on m´as amigable de la sintaxis de Haskell.
http://www.cs.uu.nl/%7Eafie/haskell/tourofsyntax.html
[5] A gentel introduction to Haskell, uno de los tutoriales m´as famosos y bien completo. Incluye m´as temas que los que vamos a ver en la materia.
http://www.haskell.org/tutorial
[6] John Hughes, Why functional programming matters, Institutionen f¨or Datavetenskap, Chalmers Tekniska H¨ogskola. Disponible en:
http://www.cs.chalmers.se/∼rjmh/Papers/whyfp.html
[7] Graham Hutton, A tutorial on the universality and expressiveness of fold, University of Nottingham, UK. Disponible en: