Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Facultad de Ciencias
Escuela de Computacin
Lecturas en Ciencias de la Computacin
ISSN 1316-6239
Laboratorio Mefis
Caracas, Febrero, 2006.
Funciones
signo(x) =
menos
cero
mas
si x < 0
si x = 0
si x > 0
signo2(x) =
menos
mas
si x < 0
si x > 0
La escogencia de cual valor del rango se produce como salida de la funcion esta determinada
por la regla de la funcion as como por el valor particular del dominio sobre el que se aplica la
funcion. Por ejemplo, signo(6) ; mas. El valor 6 que es suplido a la funcion es llamado el
par
ametro actual. Diremos que la expresion signo(6) se evalua ; a mas.
El proceso de suplir un parametro actual a la funcion se llama la aplicaci
on de la funcion y
diremos que la funcion se aplica a ese parametro actual. Llamaremos argumento de la funcion
al parametro formal o actual dependiendo del contexto en que hablemos.
La vision de una funcion como una regla para transformar entradas a salidas es fundamental
en la programacion funcional. Las cajas negras constituyen los bloque de construcci
on para un
programa funcional y uniendo estos bloque juntos se construyen operaciones mas sofisticadas.
Este proceso de acoplar las cajas se llama composici
on funcional.
Para ilustrar el proceso de composicion de funciones, consideremos la funcion max que computa el maximo de un par de n
umeros m y n:
max(m, n) =
m
n
si m > n
si m n
Ahora podemos olvidarnos de los detalles internos de esta nueva caja negra y usarla como
una unidad computacional o bloque de construccion para otras funciones. Por ejemplo, para
construir una funcion que calcule el signo del maximo de cuatro n
umeros a, b, c y d utilizando
las funciones signo y max que ya hemos definido:
SM4(a, b, c, d) = signo(max(a, max3(b, c, d)))
y que podemos representar como una caja negra de la siguiente manera:
Resumiendo: una funcion puede ser vista como una caja negra para resolver un problema y
las funciones pueden ser acopladas juntas para definir funciones mas grandes y poderosas que a
su vez pueden ser vistas como cajas negras para construir funciones a
un mas grandes.
En programacion funcional, dado un conjunto de cajas negras predefinidas llamadas funciones
primitivas que hacen las operaciones basicas, construimos nuevas funciones cajas negras que
hacen cosas mas sofisticadas en terminos de estas primitivas1 .
Luego podemos usar estas nuevas funciones como bloques de construccion para funciones mas
sofisticadas que a su vez pueden ser utilizadas de la misma manera para definir otras funciones, y
as sucesivamente hasta lograr construir la funcion deseada que resuelve el problema planteado.
Podemos definir nuevas funciones tanto para simplificar la definicion de una funcion mas
complicada como para describir una operacion utilizada com
unmente y as evitar escribir la
misma expresion una y otra vez.
Transparencia Referencial
La propiedad fundamental de las funciones matematicas que nos permite acoplar las cajas negras
de la manera antes descrita es la transparencia referencial que significa que cada expresion
denota un u
nico valor que no puede ser cambiado evaluando la expresion o permitiendo que
diferentes partes del programa compartan la expresion.
La evaluacion de una expresion simplemente cambia la forma de la expresion pero nunca su
valor. Todas las referencias a la expresion son por lo tanto equivalentes al valor mismo y el hecho
que la expresion pueda ser referenciada desde otras partes del programa no altera ese valor.
Por su transparencia referencial, una funcion matematica puede ser vista como una caja negra
que computa valores de salida solamente en terminos de sus valores de entrada. Esta propiedad es
lo que distingue a las funciones matematicas de las funciones que se pueden escribir en lenguajes
de programacion imperativa.
En los lenguajes imperativos se permite que las funciones referencien datos globales y se
permiten asignaciones destructivas a esos datos que pueden cambiar sus valores de una invocacion
a otra. Tales cambios dinamicos de los valores de los datos globales se llaman efectos laterales
y debido a ellos el valor producido por una funcion puede variar a
un cuando sus argumentos sean
los mismos cada vez que es invocada.
La presencia de efectos laterales hace que la funcion sea difcil de entender pues para determinar cual valor generara la funcion se debe considerar el valor actual de los datos globales. Esto
a su vez requiere considerar la historia de la computacion desde el comienzo de la ejecucion del
programa hasta el momento actual. Por esto es que se dice que los lenguajes imperativos son
referencialmente opacos.
Para ilustrar la opacidad referencial de los lenguajes imperativos consideremos el siguiente
programa escrito en una sintaxis tipo Pascal.
1
En el lenguaje Haskell estas funciones basicas predefinidas estan contenidas en el archivo prelude que se
carga al inicializarse el interpretador Hug.
programa ejemplo ;
var flag: booleana;
funcion f ( n : entero) : entero;
comenzar
si flag ent f := n dlc f := 2 * n;
flag := not flag
fin;
comenzar
flag := verdad;
escribir(f(1) + f(2));
escribir(f(2) + f(1))
fin.
Al ejecutar este programa obtenemos como salida los n
umeros 5 y 4. En apariencia no se
estara cumpliendo la ley conmutativa de la suma, pues la expresion f (1) + f (2) estara dando
un resultado diferente a f (2) + f (1). La causa de la anomaa es la asignaci
on destructiva
f lag := not f lag.
La asignacion destructiva no esta permitida en el razonamiento matematico el cual esta
basado en la nocion de la igualdad y en el reemplazo de una expresion por otra que signifique la
misma cosa, es decir, que denote el mismo valor. Por ejemplo, podemos reemplazar la expresi
on
4 + 5 por 9 porque ambas expresiones son denotaciones del mismo valor (el n
umero 9).
Una caracterstica de la programacion funcional es que no existen asignaciones destructivas.
En vez de considerar a las variables como receptaculos para valores que pueden ser actualizados
periodicamente por medio de la asignacion de diferentes valores, las variables en un programa
funcional son como las variables matematicas: si existen tienen un valor que no puede cambiar.
Por lo tanto, no hay nocion de estado del programa ni de historia del programa.
En la programacion funcional un programa no es un secuencia de imperativas que describen al
computador como debe resolver un problema en terminos de cambios de estado (modificaciones
a valores de variables) sino que un programa funcional describe que es lo que se va a computar, es
decir, un programa funcional es una expresi
on definida en terminos de funciones primitivas
y otras definidas por el usuario. El valor de la expresion constituye el resultado del programa.
Definici
on de Funciones
3.1
Definici
on por Combinaci
on
La forma mas facil y natural de definir funciones es por combinacion utilizando otras funciones:
max :: Int -> Int -> Int
max a b = if a > b then a else b
max3 :: Int -> Int -> Int -> Int
max3 a b c = max a (max b c)
6
3.2
Definiciones Locales
3.3
Definici
on por Casos
3.4
Definici
on por Patrones
Los parametros formales en una definicion de funcion no estan restringidos a ser solo nombres.
Ellos pueden ser patrones. Las siguientes construcciones se pueden utilizar como patrones:
N
umeros. Por ejemplo: 3
Las constantes True y False
Nombres. Por ejemplo: x
El smbolo dont care
Listas cuyos elementos tambien son patrones. Por ejemplo: [1,x,y]
El operador : con patrones a la izquierda y a la derecha. Por ejemplo: a:b
Formalmente, un patron es un termino lineal, es decir, en el cual no se repiten las variables.
Ejemplos de funciones definidas por analisis de patrones:
and :: Bool -> Bool -> Bool
and True x = x
and False = False
lon :: [a] -> Int
lon [] = 0
lon ( :y) = 1 + (lon y)
cab :: [a] -> a
cab (x: ) = x
cab [ ] = error "invalido"
cola :: [a] -> [a]
cola ( :x) = x
cola [ ] = error "invalido"
3.5
Definici
on Recursiva
En la definicion de una funcion se pueden usar las funciones estandares, las funciones definidas
previamente por el usuario, las definiciones locales y tambien la propia funcion que se define en
la definicion. Cuando ocurre esto u
ltimo la llamamos una definici
on recursiva.
Las definiciones recursivas requieren las siguientes dos condiciones:
1. Existe al menos una definicion no recursiva para un caso base.
2. El parametro de la llamada recursiva es mas simple que el parametro de la funcion que se
quiere definir. Formalmente, la secuencia de parametros en las llamadas recursivas debe
ser un conjunto bien fundado, es decir, no deben existir cadenas decrecientes infinitas.
Ejemplos de definiciones recursivas:
fact :: Int -> Int
fact n | n==0 = 1
| n>0 = n * fact (n-1)
8
3.6
Definiciones An
onimas
En vez de utilizar ecuaciones para definir funciones, tambien podemos definirlas anonimamente
va abstracciones lambda:
\x -> x + 1
Tambien podemos definir anonimamente funciones de mas de un parametro:
\x y -> x + y que es equivalente a \x -> \y -> x + y
Una definicion anonima puede utilizarse por si misma o asociarse al nombre de una funcion:
inc = \x -> x + 1
mas = \x y -> x + y
En general, si x tiene tipo t1 y exp tiene tipo t2 , entonces \x -> exp tiene tipo t1 -> t2 .
3.7
Composici
on de Funciones
f . g = \x -> f (g x)
Currificaci
on
Por lo tanto, dado un solo parametro, max devuelve una funcion con tipo Int -> Int.
Llamar una funcion con menos parametros de los que espera se llama instanciar o aplicar
parcialmente la funcion.
En realidad, en los lenguajes funcionales, existen solamente funciones de un parametro. Estas
funciones pueden devolver otra funcion que tiene un parametro. De esta manera, parece que la
funcion original tiene dos o mas parametros.
Esta estrategia de describir las funciones de mas de un parametro por funciones de un
parametro que devuelven otra funcion, se llama currificaci
on. La funcion devuelta se llama
funci
on currificada. Este metodo se debe a Moses Schonfinkel y Haskell Curry.
4.1
Secciones de Operadores
Como los operadores infijos en realidad son funciones, tiene sentido poder aplicarlos parcialmente
tambien. En programacion funcional, la aplicacion parcial de un operador infijo se llama una
seccion. Por ejemplo:
(x+)
\y -> x + y
(+y)
\x -> x + y
(+) \x y -> x + y
La u
ltima forma de seccion esencialmente coerciona un operador infijo en un equivalente valor
funcional y es conveniente cuando se necesita pasar un operador infijo como un argumento a una
funcion. Por ejemplo:
foldr (+) 0 [1,2,3] ; (1+(2+(3+0)))
As como podemos coercionar a un operador infijo a un valor funcional, tambien podemos
hacer lo contrario simplemente encerrando entre comillas izquierdas (backquotes) un identificador
asociado a un valor funcional binario. Por ejemplo:
x mas y es equivalente a mas x y
10
11
Todas las expresiones de un lenguaje funcional tienen tipo. En el caso especial de una funcion,
este puede ser especificado en su definicion.
En la programacion funcional se describe una funcion en dos pasos:
(1) describimos el tipo de la funcion el cual es una especificacion explcita del dominio y rango
de la funcion;
(2) describimos que hace la funcion en terminos de una regla que especifica que debe hacer la
funcion con sus argumentos para generar el resultado requerido.
Por ejemplo, en haskell:
(1)
(2)
::=
|
|
|
|
|
tipo estandar
var tipo
exp tipo -> exp tipo
( exp tipo , exp tipo )
[exp tipo]
... tipos definidos por el programador ...
6.1
Polimorfismo
El polimorfismo significa que una funcion puede ser aplicada a una variedad de tipos de argumentos. En los lenguajes imperativos tenemos especies limitadas de polimorfismo:
Sobrecarga de operadores, tales como los operadores aritmeticos, donde una misma operaci
on como la suma o la resta puede ser aplicada a tipos Integer, Real, etc.
Polimorfismo Ad hoc, cuando dos operaciones diferentes tienen el mismo nombre, por
ejemplo, el + para la suma y para la concatenacion de cadenas. Este polimorfismo es
confuso y puede ser fuente de muchos errores.
Polimorfismo por plantillas o templates, que se utiliza en lenguajes como C++ o Java y
consiste que una funcion o metodo pueda definirse varias veces con el mismo nombre y
diferentes tipos de parametros. A tiempo de llamada y de acuerdo al tipo de los parametros
actuales, se selecciona el cuerpo de la funcion o metodo a ser ejecutado.
En los lenguajes funcionales, se utiliza polimorfismo parametrizado donde una operacion
es aplicable a todas las situaciones consistentes con la especificacion de la funcion:
mas :: Num a => a -> a -> a
en este caso, la funcion mas es aplicable a cualquier tipo numerico y el parametro de tipo a es
reemplazado consistentemente con el mismo tipo de acuerdo a la aplicacion de la funcion:
mas :: Int -> Int -> Int
El polimorfismo parametrizado es muy poderoso porque permite expresar una funcion en
terminos generales no dependiendo de las caractersticas particulares del parametro seleccionado.
Por ejemplo, podemos contar la longitud de una lista u ordenar la lista sin necesidad de saber el
tipo de datos de los elementos almacenados en la lista:
lon :: [a] -> Int
Listas
Las listas o secuencias se usan para agrupar varios elementos del mismo tipo. Para cada tipo
existe un tipo lista de tipo . Por tanto, existen listas de enteros, lista de floats, listas de
funciones de entero a entero, etc.:
[1, 2, 3, 0] :: [Int]
[x>y, true, 1/=a] :: [Bool]
[\x -> x + 1, fac, (\x y -> x*y) 2] :: [Int -> Int]
Tambien se pueden agrupar en una lista listas del mismo tipo. El resultado sera, por ejemplo,
lista de listas de enteros, lista de listas de booleanos, etc.:
[[1, 2, 3], [0, -1, 4], [9]] :: [[Int]]
[[[sin, cos]], [[tan, cos], [sin]]] :: [[[Float -> Float]]]
7.1
Construcci
on de listas
Enumeraci
on
Constructor :
Otra manera de construir listar es por medio del operador constructor :. Este operador a
nade
un elemento al principio de una lista y construye de esta manera una lista m
as larga:
1 : [2, 3, 4, 5] ; [1, 2, 3, 4, 5]
El constructor : tiene tipo
(:) :: a -> [a] -> [a]
Usando la lista vaca y el operador : se puede construir cualquier lista:
1 : (2 : (3 : [ ])) es la lista [1, 2, 3]
El operador : asocia por la derecha:
1 :
2 :
3 : []
14
1 : (2 : (3 : [ ]))
7.1.3
Intervalos num
ericos
Se pueden construir listas con la notracion de intervalos: dos expresiones numericas separadas
por dos puntos y rodeadas de corchetes:
[1 .. 5] es la lista [1, 2, 3, 4, 5]
El valor de la expresion [x..y] se calcula por una llamada de la funcion enumFromTo x y
cuya definicion es:
enumFromTo :: Enum a => a -> a -> [a]
enumFromTo x y | y < x = [ ]
| otherwise = x : enumFromTo (x+1) y
7.1.4
Por comprensi
on
La comprension de listas es otra manera de definir listas y operaciones sobre ellas. Es una forma
natural de definicion utilizada en teora de conjuntos. Por ejemplo, para definir el conjunto de
enteros pares entre 0 y 20 escribimos:
[x | x <- [0 .. 20], even x]
donde <- significa y la coma es la conjuncion logica. De esta forma se puede escribir casi todo
lo que se pueda expresar como un conjunto. La frase x <- xs se llama un generador y puede
haber mas de uno en una definicion. Por ejemplo, para multiplicar los elementos de una lista
por los de otra, podemos definir la siguiente funcion:
mul xs ys = [x*y | x <- xs, y <- ys]
7.2
Las funciones sobre listas mas usuales son recursivas y definidas por medio de patrones, es decir,
son funciones definidas inductivamente. Puesto que cada lista o es vaca o tiene un primer
elemento x que esta delante de una lista xs (que puede ser vaca), la funcion se define para el
caso base de la lista vaca [ ] y para una lista que tiene la forma x : xs. En el caso recursivo,
con el patron x : xs la funcion se llama a si misma con el parametro xs que es menor que x : xs
cumpliendo as el requerimiento del buen fundamento en la recursion.
Estudiemos ahora algunas definiciones de funciones sobre lista.
7.2.1
Se pueden comparar y ordenar listas entre si con la condicion que se puedan comparar y ordenar
sus elementos. Es decir, el tipo [ ] pertenece a los tipos comparables Eq si esta en Eq y el tipo
[ ] esta en la clase Ord de los tipos ordenables si esta en Ord.
Dos listas son iguales si tienen exactamente los mismos elementos en el mismo orden:
15
igl [ ] [ ]
= True
igl (x:xs) (y:ys) = x == y && (igl xs ys)
igl
= False
El operador == de igualdad sobre listas se define de la siguiente manera:
(==) :: Eq
[ ] ==
[ ] ==
(x:xs) ==
(x:xs) ==
Si se pueden ordenar los elementos de una lista con <, <=, etc., tambien se pueden ordenar
entre si las listas seg
un el orden lexicogr
afico. El primer elemento de las listas decide el orden de
las listas, a no ser que sean iguales. En ese caso, decide el segundo elemento, a no ser que sean
iguales, etc. Si una de las listas es el comienzo de la otra, la mas corta en la menor. Por tanto,
las siguientes expresiones son verdaderas:
[2,3] < [3,1]
/= ys
>= ys
< ys
> ys
=
=
=
=
Concatenaci
on de listas
Se pueden unir dos listas del mismo tipo en una sola lista con el operador ++. Esta operacion se
llama concatenaci
on:
[1,2,3] ++ [4,5] ; [1,2,3,4,5]
El elemento neutro de la concatenacion es la lista vaca [ ].
El operador ++ es una funcion estandar que se realiza en el preludio y puede definirse as:
(++) :: [a] -> [a] -> [a]
[ ] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
16
Ejercicio: Definir la funcion concat que se aplica a una lista de listas. Todas las listas en la
lista de listas se concatenan en una sola lista:
concat [[1,2,3], [4,5], [ ], [6]] ; [1,2,3,4,5,6]
7.2.3
Selecci
on de partes de una lista
En el preludio se definen varias funciones que sirven para seleccionar partes de una lista. Para
algunas funciones el resultado es una sublista de la lista inicial, en otras es un elemento de ella.
Ya que una lista se construye con una cabeza y una cola, es facil recuperar estos componentes:
head :: [a] -> a
head (x:xs) = x
tail :: [a] -> [a]
tail (x:xs) = xs
La funcion recursiva last selecciona el u
ltimo elemento de una lista:
last :: [a] -> a
last [x] = x
last (x:xs) = last xs
Note que las tres funciones anteriores no estan definidas para una lista vaca, si se les llama
con [ ] como parametro, el resultado es un mensaje de error.
La funcion init selecciona todo excepto el u
ltimo elemento de una lista:
init :: [a] -> [a]
init [x] = [ ]
init (x:xs) = x : init xs
La funcion take tiene ademas de una lista, un entero como parametro el cual determina
cuantos de los primeros elementos de la lista estar
an en el resultado:
take
take
take
take
17
Propiedades de listas
7.3
Las funciones pueden ser mas flexibles si utilizan funciones como parametros. Muchas funciones
sobre listas tienen una funcion como parametro, es decir, son funciones de orden superior.
7.3.1
map
La funcion map aplica su parametro funcion a todos los elementos de una lista. Por ejemplo:
map (\x -> x * x) [1,2,3] ; [1,4,9]
La definicion de map es:
La funcion map se usa para definir otras funciones del preludio tal como elem:
elem e xs = or (map (==e) xs)
Tambien se puede escribir la definicion utilizando el operador de composicion de funciones:
elem e = or . (map (==e))
De igual forma, la funcion notElem:
notElem e = and . (map (/=e))
7.3.2
foldr
La funcion foldr inserta un operador entre todos los elementos de una lista, empezando a la
derecha con un valor dado que es el resultado para la lista vacia:
foldr (+) 0 [1,2,3,4,5] ; (1 + (2 + (3 + (4 + (5 + 0)))))
La definicion de foldr:
7.3.3
foldl
La funcion foldl inserta un operador entre todos los elementos de una lista empezando a la
izquierda de la lista. Tiene un parametro extra que indica cual es el resultado para la lista vaca:
foldl (+) 0 [1,2,3,4,5] ; (((((0 + 1) + 2) + 3) + 4 + 5)
La definicion de foldl:
Para operadores asociativos como + no importa mucho si se usa foldr o foldl. Para operadores no asociativos como - el resultado con foldr puede ser diferente al obtenido con foldl.
7.3.4
filter
La funcion filter elimina los elementos de una lista que no cumplan con una condicion especificada como una funcion booleana:
filter even [1,2,3,4,5] ; [2,4]
La definicion de filter:
7.3.5
takeWhile
Una variante de la funcion filter es la funcion takeWhile. Esta funcion tiene como parametros
un predicado (funcion con resultado de tipo Bool) y una lista. La funcion takeWhile empieza
inspeccionando al principio de la lista y termina de buscar cuando encuentra un elemento que
no satisface el predicado:
takeWhile even [2,4,6,7,8,9] ; [2,4,6]
note que el elemento 8 no esta en el resultado, mientras que con la funcion filter s estara.
La definicion de takeWhile:
7.3.6
dropWhile
La funcion dropWhile elimina la parte inicial de una lista que cumple con una condici
on:
dropWhile even [2,4,6,7,8,9] ; [7,8,9]
La definicion de dropWhile:
7.4
Ordenamiento de listas
Todas las funciones sobre listas que hemos visto hasta ahora son O(n) porque cada elemento de
la lista se visita recursivamente cuando mas una vez para determinar el resultado. Una funcion
que no se puede escribir de esta manera es el ordenamiento ascendente de una lista ya que se
tienen que cambiar las posiciones de muchos de los elementos de la lista.
Consideraremos tres algoritmos de ordenamiento de listas. En todos, es necesario que se
puedan ordenar los elementos de las listas. Por tanto, se puede ordenar una lista de enteros, o
una lista de listas de enteros, pero no es posible ordenar una lista de funciones. Expresamos este
requerimiento en el tipo de la funcion sort:
sort :: Ord a => [a] -> [a]
sort opera sobre listas de cualquier tipo a con la condicion que el tipo a este en la clase de los
tipos cuyos elementos se pueden ordenar (Ord).
20
7.4.1
Suponiendo que tenemos una lista ya ordenada, podemos insertar un nuevo elemento en el lugar
apropiado con la siguiente funcion:
insert
insert
insert
|
|
[2,4,5,6,8,10].
La funcion insert se puede usar para ordenar una lista que no este ordenada: se puede
empezar con una lista vaca e insertar primero el u
ltimo elemento de la lista; el resultado es una
lista ordenada en la que se puede insertar el pen
ultimo elemento de la lista, obteniendo una lista
de los elementos ordenada; y as sucesivamente hast insertar el primer elemento de la lista y
obteniendo entonces una lista ordenada con todos los elementos de la lista original:
x1 insert (x2 insert (. . .(xn1 insert (xn insert [ ])). . .))
La estructura de esta expresion es exactamente la de foldr con insert como operador y [ ]
como valor inicial. Por lo tanto, el algoritmo de ordenamiento por insercion lo podemos escribir:
insertsort = foldr insert [ ]
7.4.2
La siguiente funcion merge sirve para fusionar dos listas ya ordenadas en una lista ordenada:
merge
merge
merge
merge
Al igual que insert, merge espera que sus parametros esten ordenados.
El mergesort se basa en que la lista vaca y las listas con solo un elemento siempre estan
ordenadas. Ademas, una lista mas grande puede ser dividida en dos partes de (casi) el mismo
tama
no las cuales pueden ser ordenadas por llamadas recursivas a mergesort y finalmente, las
dos sublistas ordenadas son fusionadas utilizando la funcion merge:
mergesort xs
| medio < 1 = xs
| otherwise = merge (mergesort ys) (mergesort zs)
where ys = take medio xs
zs = drop medio xs
medio = (length xs) / 2
21
7.4.3
Quicksort
Tuplas
Cada elemento de una lista debe ser del mismo tipo. Sin embargo, hay situaciones donde es
necesario agrupar elementos de diferentes tipos. Por ejemplo, la informacion del registro de una
persona puede contener nombres (cadenas), sexo (booleano), fecha de nacimiento (enteros), etc.
Estos datos se corresponden, pero no es posible ponerlos en una lista. Para esas situaciones,
existe otra manera de construir tipos compuestos: las tuplas.
Una tupla consiste de un n
umero fijo de valores que pueden ser de diferentes tipos y que estan
agrupados como una entidad.
Las tuplas se escriben entre parentesis, sus elementos separados por comas:
(1, a)
([1,2], sqrt)
(1, (2,3))
Para cada combinacion de tipos se crea un nuevo tipo. El orden de los elementos es importante. El tipo de una tupla esta definido por los tipos de sus elementos entre parentesis:
(1, a) :: (Int, Char)
("pepe", False, 45) :: ([Char], Bool, Int)
([1,2], sqrt) :: ([Int], Float -> Float)
(1, (2,3)) :: (Int, (Int,Int))
No existen tuplas de un elemento: (7) es el entero 7.
8.1
:: (a,b) -> a
(x,y) = x
:: (a,b) -> b
(x,y) = y
splitAt
splitAt
splitAt
splitAt
Definici
on de Tipos
Cuando se usan mucho las listas y tuplas, las declaraciones de tipo pueden llegar a ser muy
complicadas. En esos casos, una definici
on de tipo puede ser muy u
til ya que es posible dar un
nombre a un tipo, por ejemplo:
type Punto = (Float, Float)
Con esta definicion se pueden escribir las declaraciones de tipo mas claras:
distancia :: Punto -> Float
diferencia :: Punto -> Punto -> Float
sup poligono :: [Punto] -> Float
trans poligono :: (Punto -> Punto) -> [Punto] -> [Punto]
Mejor todava si hacemos una definicion de tipo para polgono:
Type Poligono = [Punto]
sup poligono :: Poligono -> Float
trans poligono :: (Punto -> Punto) -> Poligono -> Poligono
En las definiciones de tipos el nombre de un tipo debe comenzar por una letra may
uscula.
Este nombre es utilizado como una abreviatura. Por ejemplo, si se pide al interpretador el tipo
de una expresion, mostrara (Float, Float) en vez de Punto.
Si se dan dos nombres a un tipo, por ejemplo: type Complejo = (Float, Float), entonces
se pueden usar los dos nombres indistintamente: un Punto es lo mismo que un Complejo y este
es lo mismo que un (Float, Float).
Mas adelante se vera como definir un tipo realmente nuevo.
10
Listas Infinitas
El n
umero de elementos de una lista puede ser infinito. Por ejemplo, la siguiente funcion devuelve
una lista infinita:
desde :: Num a => a -> [a]
desde n = n : desde (n+1)
Una lista infinita se puede usar como resultado intermedio de una computacion aunque el
resultado final sea finito. Por ejemplo, para calcular todas las potencias de 3 menores que 1000:
takeWhile (<1000) (map (3^ ) (desde 1))
[3, 9, 27, 81, 243, 729]
Este metodo puede ser aplicado gracias a que el interpretador es perezoso: siempre trata de
aplazar el trabajo lo mas posible. Por eso, no se calcula el resultado de map (3^ ) (desde 1)
completamente (no podra hacerlo pues tardara un tiempo infinito). Sino que primero calcula el
primer elemento de la lista. Este se pasa a takeWhile. Solamente si se ha utilizado este elemento
y takeWhile pide el siguiente, se calcula el segundo elemento. Y as sucesivamente hasta que en
alg
un momento takeWhile no pedira el siguiente elemento (cuando acabe de procesar el primer
elemento 1000). Por lo tanto, los otros elementos no seran calculados por map.
23
10.1
Evaluaci
on perezosa
La manera como se calculan las expresiones en un lenguaje se llama el metodo de evaluacion del
lenguaje. En los lenguajes funcionales modernos como Haskell, Gofer, Miranda, etc., se utiliza
el metodo conocido como evaluaci
on perezosa donde solo se calcula una expresion si realmente
se necesita su valor.
Lo opuesto a evaluacion perezosa es la evaluaci
on voraz o estricta en la cual se calculan
completamente los parametros actuales para evaluar una funcion.
Las listas infinitas son posibles gracias a la evaluacion perezosa. En los lenguajes que usan
evaluacion voraz como son todos los lenguajes imperativos y algunos funcionales (Ml o Caml), las
listas infinitas no son posibles.
Una ventaja de la evaluacion perezosa se aprecia en el siguiente ejemplo:
divisible :: Int -> Int -> Bool
divisible x y = x rem y == 0
divisores :: Int -> [Int]
divisores x = filter (divisible x) [1..x]
primo :: Int -> Bool
primo x = divisores x == [1,x]
Con la evaluacion perezosa no se calculan todas los divisores de x y se compara el resultado
con [1,x] a menos que esto sea en verdad necesario (cuando x en realidad es primo y en este
caso, solo habran dos divisores).
10.2
En el preludio estan definidas algunas funciones que devuelven listan infinitas. La funcion desde
se llama en realidad enumFrom :: Enum a => a -> [a]. Tambien podemos escribir [n..] que
es equivalente a enumFrom n (o desde n).
Una lista infinita en la cual se repite un u
nico elemento puede ser definida con la funcion
repeat :: a -> [a]
repeat x = x : repeat x
Una lista infinita generada por repeat puede ser usada como resultado intermedio por una
funcion que tiene un resultado finito:
copy :: Int -> a -> [a]
copy n x = take n (repeat x)
Gracias a la evaluacion perezosa, copy puede usar el resultado infinito de repeat.
Una funcion de orden superior que devuelve una lista infinita:
iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)
El resultado es una lista infinita en que cada siguiente elemento es el resultado de la aplicacion
de la funcion al elemento anterior:
take 10 (iterate (*2) 1) ; [1,2,4,8,16,32,64,128,256,512]
24
10.3
11
Las listas y las tuplas son dos maneras primitivas de estructurar datos. Si ellas no ofrecen todo
lo que se necesita para representar determinada informacion, se puede definir un tipo de dato.
Un tipo de dato esta caracterizado por la forma en que se pueden construir los elementos del
tipo. Por ejemplo, una lista es una estructura lineal que se puede construir como la lista vaca o
aplicando el operador :. Algunas veces no se requiere una estructura lineal sino un arbol que no
es un tipo primitivo pero que se puede declarar por medio de una definici
on de datos.
11.1
Definiciones de Datos
Las funciones que se usan para construir una estructura de datos se llaman funciones constructoras. En una definicion de datos se especifican las funciones constructoras que se pueden usar
con el nuevo tipo. En la definicion tambien estan los tipos de los parametros de las funciones
constructoras. Por ejemplo, una definicion de datos para arboles binarios:
data Arbol a = Nodo a (Arbol a) (Arbol a)
| Hoja
Esta definicion se lee: un arbol con elementos de tipo a puede ser construido de dos formas:
1. aplicando la funcion Nodo a tres parametros (uno de tipo a y dos de tipo arbol sobre a), o
2. usando la constante Hoja.
25
Los arboles pueden formarse usando las funciones constructoras en una expresion:
Nodo 4 (Nodo 2 (Nodo 1 Hoja Hoja) (Nodo 3 Hoja Hoja))
(Nodo 6 (Nodo 5 Hoja Hoja) (Nodo 7 Hoja Hoja))
Las funciones sobre arboles pueden definirse por patrones con las funciones constructoras.
Por ejemplo, para contar el n
umero de elementos en un arbol:
tama~
no :: Arbol a -> Int
tama~
no Hoja = 0
tama~
no (Nodo x p q) = 1 + tama~
no p + tama~
no q
Pudimos haber definido el tipo Arbol de otras formas. Por ejemplo, los arboles cuyo n
umero
de ramas a partir de una nodo son variables:
data Arbolv a = Nodov a [Arbolv a]
11.2
Tipos finitos
Las funciones constructoras en una definicion de tipo de dato pueden no tener ning
un parametro
como vimos con la funcion constructora Hoja del tipo Arbol. Tambien es posible que ninguna
funcion constructora tenga parametros. En esta situacion, el resultado es un tipo finito.
Un tipo finito es aquel que todas sus funciones constructoras son constantes que indican los
u
nicos elementos del tipo. Por ejemplo:
data Bool = True | False
data Dir = Norte | Sur | Este | Oeste
Se pueden escribir funciones para estos tipos con el uso de patrones:
mover
mover
mover
mover
mover
11.3
Uni
on de tipos
Utilizando las definciones de tipos, es posible, por ejemplo, que una lista pueda tener elementos
de tipo Int y de tipo Char:
data IntChar = Ent Int | Car Char
xs :: [IntChar]
xs = [Ent 1, Car a, Ent 2, Car b]
El u
nico precio es que se tiene que marcar cada elemento con una funcion constructora Ent
o Car. Estas funciones pueden interpretarse como funciones de conversion:
Ent :: Int -> IntChar
Car :: Char -> IntChar
26
Bibliografa
- Cousineau, G & Huet, G.
THE CAML PRIMER
http://caml.inria.fr/, 1997
- Hudak, P. & Peterson, J. & Fasel, J.
A GENTLE INTRODUCTION TO HASKELL 98
http://haskell.org/tutorial/, 2000
- Simon Peyton Jones
HASSKELL 98 LANGUAGE AND LIBRARIES - THE REVISED REPORT
http://www.haskell.org/haskellwiki/Definition, 2003
- Jones, M. & Reid A.
HUGS 98 USERS GUIDE
Yale Haskell Group
http://cvs.haskell.org/Hugs/pages/users guide/index.html, 2004
- Jones, M. & Reid A.
THE HUGS 98 USER MANUAL
Yale Haskell Group
http://cvs.haskell.org/Hugs/pages/hugsman/index.html, 2002
- Paulson, L.
ML FOR THE WORKING PROGRAMMER
Cambridge University Press, 2nd. edition, 1998.
Febrero 11, 2006
27