• No se han encontrado resultados

Programación Funcional en Haskell

N/A
N/A
Protected

Academic year: 2021

Share "Programación Funcional en Haskell"

Copied!
13
0
0

Texto completo

(1)

Programaci´

on Funcional en Haskell

Paradigmas de Lenguajes de Programaci´on

1◦ 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.

(2)

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_.

(3)

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)

(4)

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

(5)

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

(6)

? 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..]

(7)

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.

(8)

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)

(9)

? 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

(10)

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 : []))) =

(11)

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.

(12)

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 Haskell

www.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.

(13)

[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:

Referencias

Documento similar

Calcula probabilidades asociadas a una distribución binomial a partir de su función de probabilidad, de la tabla de la distribución o mediante calculadora,?. hoja de cálculo

MUESTRA.. 3º.- Y como tercera nota del régimen de incriminación, cabe resaltar que, por imperativo legal art 31 quinquies CP, se establece que las disposiciones relativas a

Forzando el símil para el tema que aquí nos ocupa, esa perspectiva supondría concebir la ciudad como un texto, la traza urbana como predicado de la misma y sus

El inconveniente operativo que presenta el hecho de que la distribuci´ on de probabilidad de una variable aleatoria es una funci´ on de conjunto se resuelve mediante el uso de

Mientras Phycis blennoides y Lepidion guentheri, especies que capturan presas grandes (tabla 4-8), poseen branquispinas cortas, tal como indica Rauther (1937) para este tipo de

Los cooperantes de Infantil y Audición y Lenguaje siguen en la línea de que los supervisores entusiasman a los alumnos para que comparen su pensamiento sobre la enseñanza con el

“La inclusión social de la discapacidad como tecnología biopolítica: una reflexión para el Trabajo Social”. Bogotá: Departamento de Trabajo Social, Facultad de Ciencias

Este artículo busca contribuir al debate constitucional examinado: (a) las funciones que cumplen los partidos políticos en una democracia constitucional, y cómo ellas no pueden