Paradigmas de programación Haskell un lenguaje de programación

Anuncio
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Paradigmas de programación
Haskell un lenguaje de programación funcional
Elaborado por :
- Castillo, Julio Javier
Analista en Computación - Famaf
Depto. Ingeniería en Sistemas de Información – UTN - FRC
Paradigmas de Programación
1
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
INDICE
Un poco de Historia…………………………………………………………………………...… 3
El lenguaje Haskell ……………………………………………………………………………... 5
A.INTRODUCCIÓN…………………………………………………………………………...………… 5
B.¿QUE ES HASKELL?............................................................................................................................. 5
C.¿POR QUE USAR HASKELL?.............................................................................................................. 5
D.DESARROLLO FUTURO DE HASKELL……………………………………………………………. 6
E.HASKELL EN ACCION………………………………………………………………………………. 6
1-Tipos………………………………………………………………………………..………… 6
1.1 Información de tipo……………………………………………………………..…………. 7
1.2 Tipos predefinidos…………………………………………………………………………. 7
1.3 Funciones………………………………………………………………………...…………. 7
2.-El entorno de Haskell - HUGS…………………………………………………………….. 7
3. Funciones……………………………………………………………………………………. 9
4. Listas…………………………………………………………………………………………. 9
5.Tuplas ………………………………………………………………………………………… 10
6. Ecuaciones con guardas…………………………………………………………………….. 10
7.Definiciones locales……………………………………………………………………….…..10
8. Expresiones lambda……………………………………………………………………..……11
9. Disposición del código……………………………………………………………................. 11
10.-Tipos definidos por el usuario…………………………………………………………….. 12
11. Tipos Recursivos…………………………………………………………………................ 14
12.-Entrada/Salida…………………………………………………………………..................... 15
13. Sobrecarga y Clases en Haskell………………………………………………..…………. 17
14. Evaluación Perezosa(Lazy) ………………………………………………..…...…………. 19
15. Ejercicios Prácticos ………………………………………………………………………….. 19
Anexo “La enseñanza de Haskell” ..………………………………………………..…...…………….. 21
Bibliografía ……………………………………………………………………………………………… 22
Paradigmas de Programación
2
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Un poco de Historia :
Los orígenes teóricos del modelo funcional se remontan a los años 30 en los cuales Church propuso un
nuevo modelo de estudio de la computabilidad mediante el cálculo lambda. Este modelo permitía
trabajar con funciones como objetos de primera clase. En esa misma época, Shönfinkel y Curry
construían los fundamentos de la lógica combinatoria que tendrá gran importancia para la
implementación de los lenguajes funcionales.
Hacia 1950, John McCarthy diseñó el lenguaje LISP (List Processing) que utilizaba las listas como
tipo básico y admitía funciones de orden superior. Este lenguaje se ha convertido en uno de los
lenguajes más populares en el campo de la Inteligencia Artificial. Sin embargo, para que el lenguaje
fuese práctico, fue necesario incluir características propias de los lenguajes imperativos como la
asignación destructiva y los efectos laterales que lo alejaron del paradigma funcional. Actualmente ha
surgido una nueva corriente defensora de las características funcionales del lenguaje encabezada por el
dialecto Scheme, que aunque no es puramente funcional, se acerca a la definición original de
McCarthy.
En 1964, Peter Landin diseñó la máquina abstracta SECD para mecanizar la evaluación de
expresiones, definió un subconjunto no trivial de Algol-60 mediante el cálculo lambda e introdujo la
familia de lenguajes ISWIM (If You See What I Mean) con innovaciones sintácticas (operadores
infijos y espaciado) y semánticas importantes.
En 1978 J. Backus (uno de los diseñadores de FORTRAN y ALGOL) consiguió que la comunidad
informática prestara mayor atención a la programación funcional con su artículo “Can Programming
be liberated from the Von Neumann style?” en el que criticaba las bases de la programación
imperativa tradicional mostrando las ventajas del modelo funcional.
Además Backus diseñó el lenguaje funcional FP (Functional Programming) con la filosofía de definir
nuevas funciones combinando otras funciones.
A mediados de los 70, Gordon trabajaba en un sistema generador de demostraciones denominado
LCF que incluía el lenguaje de programación ML (Metalenguaje). Aunque el sistema LCF era
interesante, se observó que el lenguaje ML podía utilizarse como un lenguaje de propósito general
eficiente. ML optaba por una solución de compromiso entre el modelo funcional y el imperativo ya
que, aunque contiene asignaciones destructivas y Entrada/Salida con efectos laterales, fomenta un
estilo de programación claramente funcional. Esa solución permite que los sistemas ML compitan en
eficiencia con los lenguajes imperativos.
A mediados de los ochenta se realizó un esfuerzo de estandarización que culminó con la definición de
SML (Stándar ML). Este lenguaje es fuertemente tipado con resolución estática de tipos, definición de
funciones polimórficas y tipos abstractos. Actualmente, los sistemas en SML compiten en eficiencia
con los sistemas en otros lenguajes imperativos.
A comienzos de los ochenta surgieron una gran cantidad de lenguajes funcionales debido
a los avances en las técnicas de implementación. Entre éstos, se podrían destacar Hope, LML, Orwell,
Erlang, FEL, Alfl, etc. Esta gran cantidad de lenguajes perjudicaba el desarrollo del paradigma
funcional. En septiembre de 1987, se celebró la conferencia FPCA en la que se decidió formar un
3
Paradigmas de Programación
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
comité internacional que diseñase un nuevo lenguaje puramente funcional de propósito general
denominado Haskell.
Con el lenguaje Haskell se pretendía unificar las características más importantes de los lenguajes
funcionales. como las funciones de orden superior, evaluación perezosa, inferencia estática de tipos,
tipos de datos definidos por el usuario, encaje de patrones y listas por comprensión. Al diseñar el
lenguaje se observó que no existía un tratamiento sistemático de la sobrecarga con lo cual se construyó
una nueva solución conocida como las clases de tipos.
El lenguaje incorporaba, además, Entrada/Salida puramente funcional y definición de arrays por
comprensión.
Durante casi 10 años aparecieron varias versiones del lenguaje Haskell, hasta que en 1998 se decidió
proporcionar una versión estable del lenguaje, que se denominó Haskell98, a la vez que se continuaba
la investigación de nuevas características y nuevas extensiones al lenguaje.
A continuación veremos gráficamente la evolucion de los más importantes lenguajes de programación:
Paradigmas de Programación
4
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
El lenguaje Haskell:
A. INTRODUCCIÓN
Haskell es un lenguaje de programación. En particular, es un lenguaje de tipos polimórficos, de
evaluación perezosa, puramente funcional, muy diferente de la mayoría de los otros lenguajes de
programación.
El nombre del lenguaje se debe a Haskell Brooks Curry.
Haskell se basa en el lambda cálculo, por eso se usa lambda como un logo.
B. ¿QUE ES HASKELL?
Haskell es un lenguaje de programación moderno, estándar, no estricto, puramente funcional.
Posee todas las características avanzadas, incluyendo polimorfismo de tipos, evaluación perezosa y
funciones de alto orden. También es un tipo de sistema que soporta una forma sistemática de
sobrecarga y un sistema modular.
Está específicamente diseñado para manejar un ancho rango de aplicaciones, tanto numéricas como
simbólicas. Para este fin, Haskell tiene una sintaxis expresiva y una gran variedad de constructores de
tipos, a parte de los tipos convencionales (enteros, punto flotante y booleanos).
Hay disponible un gran número de implementaciones. Todas son gratis. Los primeros usuarios, tal vez,
deban empezar con Hugs, un intérprete pequeño y portable de Haskell.
C. ¿POR QUE USAR HASKELL?
Escribir en grandes sistemas software para trabajar es difícil y caro. El mantenimiento de estos
sistemas es aún más caro y difícil. Los lenguajes funcionales, como Haskell pueden hacer esto de
manera más barata y más fácil.
Haskell, un lenguaje puramente funcional ofrece:
1. Un incremento substacial de la productividad de los programas.
2. Código más claro y más corto y con un mantenimiento mejor.
3. Una “semántica de huecos” más pequeña entre el programador y el lenguaje.
4. Tiempos de computación más cortos.
Haskell es un lenguaje de amplio espectro, apropiado para una gran variedad de aplicaciones. Es
particularmente apropiado para programas que necesitan ser altamente modificados y mantenidos.
La vida de muchos productos software se basa en la especificación, el diseño y el mantenimiento y no
en la programación.
Los lenguajes funcionales son idóneos para escribir especificaciones que actualmente son ejecutadas
(y, por lo tanto, probadas y depuradas).Tal especificación es, por tanto, el primer prototipo del
programa final.
Paradigmas de Programación
5
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Los programas funcionales son también relativamente fáciles de mantener porque el código es más
corto, más claro y el control riguroso de los efectos laterales elimina gran cantidad de interacciones
imprevistas.
D. DESARROLLO FUTURO DE HASKELL
Haskell 98 está completo. Es la definición oficial y actual de Haskell. Se espera que este lenguaje
pueda seguir funcionando aunque se añadan nuevas extensiones y los compiladores seguirán
apoyándose en Haskell 98.
No obstante, han sido propuestas muchas extensiones que han sido implementadas en algunos sistemas
Haskell; por ejemplo el diseño de guardas, clases de tipos con multiparámetros, cuantificaciones
locales y universales, etc.
La lista de correo de Haskell es un forum de discusión sobre las características de los nuevos
lenguajes. La gente propone una nueva característica de algún lenguaje y trata de convencer a los
investigadores sobre la conveniencia de desarrollarla en algún sistema Haskell. Al final, la gente que
está interesada de verdad define esta nueva característica en Haskell 98 y lo presenta en un documento
que es añadido a esta lista de correo.
Un buen ejemplo de la utilidad de este proceso es la creación de FFI (Foreing Function Interface) en
Haskell.
Haskell II ha sido desarrollado durante mucho tiempo por un comité liderado por John Launchbury.
Obviamente las extensiones que hayan sido bien descritas ycomprobadas tendrán más probabilidad de
ser aceptadas.
E. HASKELL EN ACCION
Haskell es un lenguaje de programación fuertemente tipado (en términos anglófilos, a typeful
programming language; término atribuido a Luca Cardelli.) Los tipos son penetrantes (pervasive), y
presenta además un potente y complejo sistema de tipos. Para aquellos lectores que esten
familiarizados con Java, C, Modula, o incluso ML, el acomodo a este lenguaje será más significativo,
puesto que el sistema de tipos de Haskell es diferente y algo más rico.
1-Tipos
Una parte importante del lenguaje Haskell lo forma el sistema de tipos que es utilizado para detectar
errores en expresiones y definiciones de función.
El universo de valores es particionado en colecciones organizadas, denominadas tipos. Cada tipo tiene
asociadas un conjunto de operaciones que no tienen significado para otros tipos, por ejemplo, se puede
aplicar la función (+) entre enteros pero no entre caracteres o funciones.
Una propiedad importante del Haskell es que es posible asociar un único tipo a toda expresión bien
formada. Esta propiedad hace que el Haskell sea un lenguaje fuertemente tipado. Como consecuencia,
cualquier expresión a la que no se le pueda asociar un tipo es rechazada como incorrecta antes de la
evaluación. Por ejemplo:
f x = 'A'
g x = x + f x
Paradigmas de Programación
6
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
La expresión 'A' denota el caracter A. Para cualquier valor de x, el valor de f x es igual al caracter
'A', por tanto es de tipo Char. Puesto que el (+) es la operación suma entre números, la parte derecha
de la definición de g no está bien formada, ya que no es posible aplicar (+) sobre un caracter.
El análisis de los escritos puede dividirse en dos fases: Análisis sintáctico, para chequear la corrección
sintáctica de las expresiones y análisis de tipo, para chequear que todas las expresiones tienen un tipo
correcto.
1.1 Información de tipo
Además de las definiciones de función, en los escritos se puede incluir información de tipo mediante
una expresión de la forma A::B para indicar al sistema que “A es de tipo B”. Por ejemplo:
cuadrado:: Int -> Int
cuadrado x = x * x
//no es necesario, pero es buena práctica
La primera línea indica que la función cuadrado es del tipo "función que toma un entero y devuelve un
entero".
Aunque no sea obligatorio incluir la información de tipo, sí es una buena práctica, ya que el Haskell
chequea que el tipo declarado coincide que el tipo inferido por el sistema a partir de la definición,
permitiendo detectar errores de tipos.
1.2 Tipos predefinidos
Existen varios “tipos” predefinidos del sistema Haskell, éstos se podrían clasificar en: tipos básicos,
cuyos valores se toman como primitivos, por ejemplo, Enteros, Flotantes, Caracterses y Booleanos; y
tipos compuestos, cuyos valores se construyen utilizando otros tipos, por ejemplo, listas, funciones y
tuplas.
1.3 Funciones
En Haskell las funciones se definen usualmente a través de una colección de ecuaciones. Por ejemplo,
la función inc puede definirse por una única ecuación:
inc n = n+1
Una ecuación es un ejemplo de declaración. Otra forma de declaración es la declaración de tipo de
una función o type signature declaration, con la cual podemos dar de forma explícita el tipo de una
función; por ejemplo, el tipo de la función inc:
inc :: Integer -> Integer
2.-El entorno de Haskell - HUGS
El entorno HUGS funciona siguiendo el modelo de una calculadora en el que se establece una sesión
interactiva entre el ordenador y el usuario. Una vez arrancado, el sistema muestra un prompt "?" y
Paradigmas de Programación
7
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
espera a que el usuario introduzca una expresión (denominada expresión inicial y presione la tecla
<RETURN>. Cuando la entrada se ha completado, el sistema evalúa la expresión e imprime su valor
antes de volver a mostrar el prompt para esperar a que se introduzca la siguiente expresión.
Ejemplo:
? (2+3)*8
40
? sum [1..10]
55
En el primer ejemplo, el usuario introdujo la expresión "(2+3)*8" que fue evaluada por el sistema
imprimiendo como resultado el valor "40".
En el segundo ejemplo, el usuario tecleó "sum [1..10]". La notación [1..10] representa la
lista de enteros que van de 1 hasta 10, y sum es una función estándar que devuelve la suma de una
lista de enteros. El resultado obtenido por el sistema es:
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55
En los ejemplos anteriores, se utilizaron funciones estándar, incluidas junto a una larga colección de
funciones en un fichero denominado "estándar prelude" que es cargado al arrancar el sistema. Con
dichas funciones se pueden realizar una gran cantidad de operaciones útiles. Por otra parte, el usuario
puede definir sus propias funciones y almacenarlas en un fichero de forma que el sistema pueda
utilizarlas en el proceso de evaluación. Por ejemplo, el usuario podría crear un fichero fichero.hs con
el contenido:
cuadrado::Integer -> Integer
cuadrado x = x * x
menor::(Integer, Integer) -> Integer
menor (x,y) = if x <= y then x else y
Para poder utilizar las definiciones anteriores, es necesario cargar las definiciones del fichero en el
sistema. La forma más simple consiste en utilizar el comando ":load":
? :l fichero.hs
Reading script file "fichero.hs"
. . .
?
Si el fichero se cargó con éxito, el usuario ya podría utilizar la definición:
? cuadrado (3+4)
49
? cuadrado (menor (3,4))
9
Es conveniente distinguir entre un valor como ente abstracto y su representación, un expresión
formada por un conjunto de símbolos. En general a un mismo valor abstracto le pueden corresponder
diferentes representaciones. Por ejemplo, 7+7, cuadrado 7,
49, XLIX (49 en números romanos), 110001 (en binario) representan el mismo valor.
El proceso de evaluación consiste en tomar una expresión e ir transformándola aplicando las
definiciones de funciones (introducidas por el programador o predefinidas) hasta que no pueda
transformarse más. La expresión resultante se denomina representación canónica y es mostrada al
usuario.
8
Paradigmas de Programación
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Existen valores que no tienen representación canónica (por ejemplo, las funciones) o que tienen una
representación canónica infinita (por ejemplo, el número pi ).
Por el contrario, otras expresiones, no representan ningún valor. Por ejemplo, la expresión (1/0) o la
expresión (infinito). Se dice que estas expresiones representan el valor indefinido.
3. Funciones
Si a y b son dos tipos, entonces a->b es el tipo de una función que toma como argumento un
elemento de tipo a y devuelve un valor de tipo b.
Las funciones en Haskell son objetos de primera clase. Pueden ser argumentos o resultados de otras
funciones o ser componentes de estructuras de datos. Esto permite simular mediante funciones de un
único argumento, funciones con múltiples argumentos.
Considérese, por ejemplo, la función de suma (+). En matemáticas se toma la suma como una función
que toma una pareja de enteros y devuelve un entero. Sin embargo, en Haskell, la función suma tiene el
tipo:
(+)::Int->(Int->Int)
es una función de un argumento de tipo Int que devuelve una función de tipo Int->Int. De
hecho "(+) 5" denota una función que toma un entero y devuelve dicho entero más 5. Este proceso
se denomina currificación (en honor a Haskell B.Curry ) y permite reducir el número de paréntesis necesarios
para escribir expresiones. De hecho, no es necesario escribir f(x) para denotar la aplicación del
argumento x a la función x, sino simplemente f x.
(+)
Nota : Se podría escribir simplemente (+)::Int->Int->Int, puesto que el operador -> es asociativo a la derecha.
4. Listas
Si a es un tipo cualquiera, entonces [a] representa el tipo de listas cuyos elementos son valores de
tipo a.
Hay varias formas de escribir expresiones de listas:
- La forma más simple es la lista vacía, representada mediante [].
- Las listas no vacías pueden ser construidas enunciando explícitamente sus elementos
(por ejemplo, [1,3,10]) o añadiendo un elemento al principio de otra lista utilizando el operador de
construcción (:). Estas notaciones son equivalentes:
[1,3,10] = 1:[3,10] = 1:(3:[10]) = 1:(3:(10:[]))
El operador (:) es asociativo a la derecha, de forma que 1:3:10:[] equivale a
(1:(3:(10:[]))), una lista cuyo primer elemento es 1, el segundo 3 y el último 10.
Paradigmas de Programación
9
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
El standar prelude incluye un amplio conjunto de funciones de manejo de listas, por
ejemplo:
length xs
devuelve el número de elementos de xs
xs ++ ys
devuelve la lista resultante de concatenar xs e ys
concat xss
devuelve la lista resultante de concatenar las listas de xss
map f xs
devuelve la lista de valores obtenidos al aplicar la función
f a cada uno de los elementos de la lista xs.
Ejemplos:
? length [1,3,10]
3
? [1,3,10] ++ [2,6,5,7]
[1, 3, 10, 2, 6, 5, 7]
? concat [[1], [2,3], [], [4,5,6]]
[1, 2, 3, 4, 5, 6]
? map fromEnum ['H', 'o', 'l', 'a']
[104, 111, 108, 97]
?
Obsérvese que todos los elementos de una lista deben ser del mismo tipo. La expresión 'a',2,False]
no está permitida en Haskell.
5.Tuplas
Si t1, t2, ..., tn son tipos y n>=2, entonces hay un tipo de n-tuplas escrito (t1, t2, ..., tn)
cuyos elementos pueden ser escritos también como (x1, x2,..., xn) donde cada x1, x2, ...,xn
tiene tipos t1,t2, ..., tn respectivamente.
Ejemplo:
(1, [2], 3) :: (Int, [Int], Int)
('a', False) :: (Char, Bool)
((1,2),(3,4)) :: ((Int, Int), (Int, Int))
Obsérvese que, a diferencia de las listas, los elementos de una tupla pueden tener tipos diferentes. Sin
embargo, el tamaño de una tupla es fijo.
En determinadas aplicaciones es útil trabajar con una tupla especial con 0 elementos denominada tipo
unidad. El tipo unidad se escribe como () y tiene un único elemento que es también ().
6. Ecuaciones con guardas
Cada una de las ecuaciones de una definición de función podría contener guardas que
requieren que se cumplan ciertas condiciones sobre los valores de los argumentos.
minimo x y
| x <= y = x
| otherwise = y
En general una ecuación con guardas toma la forma:
f x1 x2 ... xn | condicion1 = e1
| condicion2 = e2
.
.
| condicionm = em
Esta ecuación se utiliza para evaluar cada una de las condiciones por orden hasta que alguna de ellas
sea "True", en cuyo caso, el valor de la función vendrá dado por la expresión correspondiente en la
parte derecha del signo "=".
Paradigmas de Programación
10
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
En Haskell, la variable "otherwise" evalúa a "True". Por lo cual, escribir "otherwise" como una
condición significa que la expresión correspondiente será siempre utilizada si no se cumplió ninguna
condición previa.
7.Definiciones locales
Las definiciones de función podrían incluir definiciones locales para variables que podrían en guardas
o en la parte derecha de una ecuación.
Considérese la siguiente función que calcula el número de raíces diferentes de una ecuación cuadrática
de la forma :
numeroDeRaices a b c | discr>0 = 2
| discr==0 = 1
| discr<0 = 0
where discr = b*b - 4*a*c
Las definiciones locales pueden también ser introducidas en un punto arbitrario de una expresión
utilizando una expresión de la forma: let <decls> in <expr>
Por ejemplo:
? let x = 1 + 4 in x*x + 3*x + 1
41
? let p x = x*x + 3*x + 1 in p (1 + 4)
41
?
8. Expresiones lambda
Además de las definiciones de función con nombre, es posible definir y utilizar funciones sin
necesidad de darles un nombre explícitamente mediante expresiones lambda de la forma:
\ <patrones atómicos> -> <expr>
Esta expresión denota una función que toma un número de parámetros (uno por cada patrón)
produciendo el resultado especificado por la expresión <expr>. Por ejemplo, la expresión:
(\x->x*x)
representa la función que toma un único argumento entero 'x' y produce el cuadrado de ese número
como resultado. Otro ejemplo sería la expresión
(\x y->x+y)
que toma dos argumentos enteros y devuelve su suma. Esa expresión es equivalente al operador (+):
? (\x y->x+y) 2 3
5
9. Disposición del código
El lector se habrá preguntado cómo es posible evitar la utilización de separadores que marquen el final
de una ecuación, una declaración, etc. Por ejemplo, la siguiente expresión:
Paradigmas de Programación
11
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
La respuesta es que el Haskell utiliza una sintaxis bidimensional denominada espaciado(layout) que se
basa esencialmente en que las declaraciones están alineadas por columnas.
Las reglas del espaciado son bastante intuitivas y podrían resumirse en:
1.- El siguiente caracter de cualquiera de las palabras clave where, let, o of es el que determina la
columna de comienzo de declaraciones en las expresiones where, let, o case correspondientes. Por
tanto podemos comenzar las declaraciones en la misma línea que la palabra clave, en la siguiente o
siguientes.
2.- Es necesario asegurarse que la columna de comienzo dentro de una declaración está más a la
derecha que la columna de comienzo de la siguiente cláusula. En caso contrario, habría ambigüedad,
ya que el final de una declaración ocurre cuando se encuentra algo a la izquierda de la columna de
comienzo.
El espaciado es una forma sencilla de agrupamiento que puede resultar bastante útil. Por ejemplo, la
declaración anterior sería equivalente a:
10. Tipos definidos por el usuario
10.1 Sinónimos de tipo
Los sinónimos de tipo se utilizan para proporcionar abreviaciones para expresiones de tipo
aumentando la legibilidad de los programas. Un sinónimo de tipo es introducido con una declaración
de la forma:
type Nombre a1 ... an = expresion_Tipo
donde
- Nombre es el nombre de un nuevo constructor de tipo de aridad n>=0
- a1,..., an son variables de tipo diferentes que representan los argumentos de
Nombre
- expresion_Tipo
a1,..., an.
es una expresión de tipo que sólo utiliza como variables de tipo las variables
Ejemplo:
type Nombre = String
type Edad = Integer
type String = [Char]
type Persona = (Nombre, Edad)
tocayos::Persona -> Persona -> Bool
Paradigmas de Programación
12
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
tocayos (nombre,_) (nombre',_) = n == nombre'
10.2 Definiciones de tipos de datos
Aparte del amplio rango de tipos predefinidos, en Haskell también se permite definir nuevos tipos de
datos mediante la sentencia data. La definición de nuevos tipos de datos aumenta la seguridad de los
programas ya que el sistema de inferencia de tipos distingue entre los tipos definidos por el usuario y
los tipos predefinidos.
10.3 Tipos Producto
Se utilizan para construir un nuevo tipo de datos formado a partir de otros.
Ejemplo:
data Persona = Pers Nombre Edad
juan::Persona
juan = Pers "Juan Lopez" 23
Se pueden definir funciones que manejen dichos tipos de datos:
esJoven:: Persona -> Bool
esJoven (Pers _ edad) = edad < 25
verPersona::Persona -> String
verPersona (Pers nombre edad) = "Persona, nombre " ++ nombre ++ ", edad: " ++ show edad
También se puden dar nombres a los campos de un tipo de datos producto:
data = Datos { nombre::Nombre, dni::Integer, edad:Edad }
Los nombres de dichos campos sirven como funciones selectoras del valor correspondiente.
Por ejemplo:
tocayos:: Persona -> Persona -> Bool
tocayos p p' = nombre p == nombre p'
Obsérvese la diferencia de las tres definiciones de Persona
1.- Como sinónimo de tipos:
type Persona = (Nombre, Edad)
No es un nuevo tipo de datos. En realidad, si se define
type Direccion = (Nombre, Numero)
type Numero = Integer
El sistema no daría error al aplicar una función que requiera un valor de tipo persona con un valor de
tipo Dirección. La única ventaja (discutible) de la utilización de sinónimos de tipos de datos podría ser
una mayor eficiencia (la definición de un nuevo tipo de datos puede requerir un mayor consumo de
recursos.
2.- Como Tipo de Datos
data Persona = Pers Nombre Edad
El valor de tipo Persona es un nuevo tipo de datos y, si se define:
type Direccion = Dir Nombre Numero
El sistema daría error al utilizar una dirección en lugar de una persona. Sin embargo, en la definición
de una función por encaje de patrones, es necesario conocer el número de campos que definen una
persona, por ejemplo:
esJoven (Pers _ edad) = edad < 25
Si se desea ampliar el valor persona añadiendo, por ejemplo, el "dni", todas las definiciones que
trabajen con datos de tipo Persona deberían modificarse.
Paradigmas de Programación
13
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
3.- Mediante campos con nombre:
data Persona = Pers { nombre::Nombre, edad::Edad }
El campo sí es un nuevo tipo de datos y ahora no es necesario modificar las funciones que trabajen con
personas si se amplían los campos.
10.4 Tipos Polimórficos
Haskell proporciona tipos polimóficos (tipos cuantificados universalmente sobre todos los tipos). Tales
tipos describen esencialmente familias de tipos.
Por ejemplo, (para_todo a)[a] es la familia de las listas de tipo base a, para cualquier tipo a. Las listas
de enteros (e.g. [1,2,3]), de caracteres (['a','b','c']), e incluso las listas de listas de interos, etc., son
miembros de esta familia. (Nótese que [2,'b'] no es un ejemplo válido, puesto que no existe un tipo que
contenga tanto a 2 como a 'b'.)
Ya que Haskell solo permite el cuantificador universal, no es necesario escribir el símbolo
correspondiente a la cuantificación universal, y simplemente escribimos [a] como en el ejemplo
anterior. En otras palabras, todas las variables de tipos son cuantificadas universalmente de forma
implícita. Las listas constituyen una estructura de datos comunmente utilizada en lenguajes
funcionales, y constituyen una buena herramienta para mostrar los principios del polimorfismo.
En Haskell, la lista [1,2,3] es realmente una abreviatura de la lista 1:(2:(3:[])), donde [] denota la lista
vacía y : es el operador infijo que añade su primer argumento en la cabeza del segundo argumento
(una lista). (: y [] son, respectivamente, los operadores cons y nil del lenguaje Lisp) Ya que : es
asociativo a la derecha, también podemos escribir simplemente 1:2:3:[].
Ejemplo : “el problema de contar el número de elementos de una lista”
length
length []
length (x:xs)
:: [a] -> Integer
= 0
= 1 + length xs
Esta definición es auto-explicativa. Podemos leer las ecuaciones como sigue: "La longitud de la lista
vacía es 0, y la longitud de una lista cuyo primer elemento es x y su resto es xs viene dada por 1 más la
longitud de xs." (Nótese el convenio en el nombrado: xs es el plural de x, y x:xs debe leerse: "una x
seguida de varias x).
pattern matching: (matcheo de patrones) En el ejemplo anterior, los miembros izquierdos de las
ecuaciones contienen patrones tales como [] y x:xs. En una aplicación o llamada a la función, estos
patrones son comparados con los argumentos de la llamada de forma intuitiva ([] solo "concuerda"
(matches) o puede emparejarse con la lista vacia, y x:xs se podrá emparejar con una lista de al menos
un elemento, instanciándose x a este primer elemento y xs al resto de la lista). Si la comparación tiene
éxito, el miembro izquierdo es evaluado y devuelto como resultado de la aplicación. Si falla, se intenta
la siguiente ecuación, y si todas fallan, el resultado es un error.
Paradigmas de Programación
14
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
11. Tipos Recursivos
Los tipos de datos pueden autorreferenciarse consiguiendo valores recursivos, por ejemplo:
data Expr = Lit Integer
| Suma Expr Expr
| Resta Expr Expr
eval (Lit n) = n
eval (Suma e1 e2) = eval e1 + eval e2
eval (Resta e1 e2) = eval e1 * eval e2
También pueden definirse tipos de datos polimórficos. El siguiente ejemplo define un tipo que
representa árboles binarios:
data Arbol a = Hoja a | Rama (Arbol a) (Arbol a)
Por ejemplo,
a1 = Rama (Rama (Hoja 12) (Rama (Hoja 23) (Hoja 13)) (Hoja 10)
tiene tipo Arbol
Integer
y representa el árbol binario de la figura :
Otro ejemplo, sería un árbol, cuyas hojas fuesen listas de enteros o incluso árboles.
a2
a2
a3
a3
::Arbol [Integer]
= Rama (Hoja [1,2,3] Hoja [4,5,6])
:: Arbol (Arbol Int)
= Rama (Hoja a1) (Hoja a1)
A continuación se muestra una función que calcula la lista de nodos hoja de un árbol binario:
hojas :: Arbol a -> [a]
hojas (Hoja h) = [h]
hojas (Rama izq der) = hojas izq ++ hojas der
Utilizando el árbol binario anterior como ejemplo:
? hojas a1
[12, 23, 13, 10]
? hojas a2
[[1,2,3], [4,5,6]]
10
12
23 13
12.-Entrada/Salida
Hasta ahora, todas las funciones descritas tomaban sus argumentos y devolvían un valor sin interactuar
con el exterior. A la hora de realizar programas "reales" es necesario que éstos sean capaces de
almacenar resultados y leer datos de ficheros, realizar preguntas y obtener respuestas del usuario, etc.
Paradigmas de Programación
15
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Una de las principales ventajas del lenguaje Haskell es que permite realizar las tareas de
Entrada/Salida de una forma puramente funcional, manteniendo la transparencia referencial y sin
efectos laterales.
Para ello, a partir de la versión 1.3 se utiliza una mónada de Entrada/Salida.
El concepto de mónada tiene su origen en una rama de las matemáticas conocida como Teoría de la
Categoría. No obstante, desde el punto de vista del programador resulta más sencillo considerar una
mónada como un tipo abstracto de datos. En el caso de la mónada de Entrada/Salida, los valores
abstractos son las acciones primitivas correspondientes a operaciones de Entrada/Salida
convencionales.
Ciertas operaciones especiales permiten componer acciones de forma secuencial (de forma similar al
punto y coma de los lenguajes imperativos). Esta abstracción permite ocultar el estado del sistema (es
decir, el estado del mundo externo) al programador que accede a través de funciones de composición o
primitivas.
Una expresión de tipo IO a denota una computación que puede realizar operaciones de
Entrada/Salida y devolver un resultado de tipo a.
A continuación se declara una sencilla función que muestra por pantalla la cadena "Hola Mundo":
main::IO()
main = print "Hola, mundo!"
La función main tiene tipo IO () indicando que realiza Entrada/Salida y no devuelve ningún valor.
Esta función tiene un significado especial cuando el lenguaje es compilado, puesto que es la primera
función evaluada por el sistema. En esta ocasión, se utiliza la función print declarada en el Standar
prelude que se encargará de imprimir su argumento en la salida estándar.
12.1 Funciones básicas de Entrada/Salida
A continuación se muestran algunas de las funciones básicas de Entrada/salida predefinidas:
12.2 Composición de Operaciones de Entrada/Salida
Paradigmas de Programación
16
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Existen dos funciones de composición de acciones de E/S. La función >> se utiliza cuando el
resultado de la primera acción no es interesante, normalmente, cuando es (). La función (>>=) pasa
el resultado de la primera acción como un argumento a la segunda acción.
(>>=) :: IO a -> (a -> IO b) -> IO b
(>>) :: IO a -> IO b -> IO b
Por ejemplo
main = readFile "fentrada" >>= \cad ->
writeFile "fsalida" (map toUpper cad) >>
putStr "Conversion realizada\n"
en este ejemplo se leen los contenidos del fichero fentrada y se escriben, convirtiendo minúsculas en
mayúsculas en fsalida.
Existe una notación especial que permite una sintaxis con un estilo más imperativo mediante la
sentencia do. Una versión mejorada del ejemplo anterior, podría reescribirse como:
main = do
putStr "Fichero de Entrada? "
fentrada <- getLine
putStr "Fichero de salida? "
fsalida <- getLine
cad <- readFile fentrada
writeFile fsalida (map toUpper cad)
putStr "Conversion realizada\n"
La función return se utiliza para definir el resultado de una operación de Entrada/Salida. Por
ejemplo, la función getLine podría definirse en función de getChar utilizando return para definir
el resultado.
getLine :: IO String
getLine = do c <- getChar
if c == '\n' then return ""
else do s <- getLine
return (c:s)
12.3 Control de excepciones
El sistema de incluye un mecanismo simple de control de excepciones. Cualquier operación de
Entrada/Salida podría lanzar una excepción en lugar de devolver un resultado. Las excepciones se
representan como valores de tipo IOError. Mediante la función userError el usuario podría lanzar
también sus propios errores.
Las excepciones pueden ser lanzadas y capturadas mediante las funciones:
fail :: IOError -> IO a
catch :: IO a -> (IOError -> IO a) -> IO a
La función fail lanza una excepción; la función catch establece un manejador que recibe cualquier
excepción elevada en la acción protegida por él. Una excepción es capturada por el manejador más
reciente. Puesto que los manejadores capturan todas las excepciones ( no son selectivos), el
programador debe encargarse de propagar las excepciones que no desea manejar. Si una excepción se
propaga fuera del sistema, se imprime un error de tipo IOError.
13. Sobrecarga y Clases en Haskell
Paradigmas de Programación
17
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Cuando una función puede utilizarse con diferentes tipos de argumentos se dice que está sobrecargada.
La función (+), por ejemplo, puede utilizarse para sumar enteros o para sumar flotantes. La resolución
de la sobrecarga por parte del sistema Haskell se basa en organizar los diferentes tipos en lo que se
denominan clases de tipos.
Considérese el operador de comparación (==). Existen muchos tipos cuyos elementos pueden ser
comparables, sin embargo, los elementos de otros tipos podrían no ser comparables. Por ejemplo,
comparar la igualdad de dos funciones es una tarea computacionalmente intratable, mientras que a
menudo se desea comparar si dos listas son iguales. De esa forma, si se toma la definición de la
función elem que chequea si un elemento pertenece a una lista:
x `elem` [] = False
x `elem` (y:ys) = x == y || (x `elem` ys)
I
Intuitivamente el tipo de la función elem debería ser a->[a]->Bool. Pero esto implicaría que la
función == tuviese tipo a->a->Bool. Sin embargo, como ya se ha indicado, interesaría restringir la
aplicación de == a los tipos cuyos elementos son comparables.
Además, aunque == estuviese definida sobre todos los tipos, no sería lo mismo comparar la igualdad
de dos listas que la de dos enteros.
Las clases de tipos solucionan ese problema permitiendo declarar qué tipos son instancias de unas
clases determinadas y proporcionando definiciones de ciertas operaciones asociadas con cada clase de
tipos. Por ejemplo, la clase de tipo que contiene el operador de igualdad se define en el standar
prelude como:
class Eq a where
(==)
x == y
:: a -> a -> Bool
= not (x /= y)
Eq es el nombre de la clase que se está definiendo, (==) y (/=) son dos operaciones simples sobre
esa clase. La declaración anterior podría leerse como:
"Un tipo a es una instancia de una clase Eq si hay una operación (==) definida sobre él".
La restricción de que un tipo a debe ser una instancia de una clase Eq se escribe Eq a.
Obsérvese que Eq a no es una expresión de tipo sino una restricción sobre el tipo de un objeto a (se
denomina un contexto). Los contextos son insertados al principio de las expresiones de tipo. Por
ejemplo, la operación == sería del tipo:
(==):: (Eq a) => a -> a -> Bool
Esa expresión podría leerse como: "Para cualquier tipo a que sea una instancia de la
clase Eq, == tiene el tipo a->a->Bool".
La restricción se propagaría a la definición de elem que tendría el tipo:
elem:: (Eq a) => a -> [a] -> Bool
Las declaraciones de instancias permitirán declarar qué tipos son instancias de una determinada clase.
Por ejemplo:
instance Eq Int where
x == y = intEq x y
La definición de == se denomina método. IntEq es una función primitiva que compara si dos enteros
son iguales, aunque podría haberse incluido cualquier otra expresión que definiese la igualdad entre
enteros. La declaración se leería como:
Paradigmas de Programación
18
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
"El tipo Int es una instancia de laclase Eq y el método correspondiente a la operación == se define
como ...".
De la misma forma se podrían crear otras instancias:
instance Eq Float where
x == y = FloatEq x y
La declaración anterior utiliza otra función primitiva que compara flotantes para indicar cómo
comparar elementos de tipo Float. Además, se podrían declarar instancias de la clase Eq tipos
definidos por el usuario. Por ejemplo, la igualdad entre elementos del tipo Arbol definido
anteriomente :
instance (Eq a) => Eq (Arbol a) where
Hoja a == Hoja b = a == b
Rama i1 d1 == Rama i2 d2 = (i1==i2) && (d1==d2)
_ == _ = False
Obsérvese que el contexto (Eq a) de la primera línea es necesario debido a que los elementos de las
hojas son comparados en la segunda línea. La restricción adicional está indicando que sólo se podrá
comparar si dos árboles son iguales cuando se puede comparar si sus hojas son iguales.
El standar prelude incluye un amplio conjunto de clases de tipos. De hecho, la clase Eq está definida
con una definición ligeramente más larga que la anterior.
class Eq a where
(==), (/=) :: a->a->Bool
x /= y = not (x == y)
Se incluyen dos operaciones, una para igualdad (==) y otra para no igualdad (/=). Se puede observar
la utilización de un método por defecto para la operación (/=). Si se omite la declaración de un
método en una instancia entonces se utiliza la declaración del método por defecto de su clase. Por
ejemplo, las tres instancias anteriores podrían utilizar la operación (/=) sin problemas utilizando el
método por defecto (la negación de la igualdad).
Haskell también permite la inclusión de clases. Por ejemplo, podría ser interesante definir una clase
Ord que hereda todas las operaciones de Eq pero que, además tuviese un conjunto nuevo de
operaciones:
class (Eq a) => Ord a where
(<), (<=), (>=), (>) :: a->a->Bool
max, min :: a->a->a
El contexto en la declaración indica que Eq es una superclase de Ord (o que Ord es una subclase de
Eq), y que cualquier instancia de Ord debe ser también una instancia de Eq.
Las inclusiones de clase permiten reducir el tamaño de los contextos: Una expresión de tipo para una
función que utiliza operaciones tanto de las clases Eq como Ord podría utilizar el contexto (Ord a)
en lugar de (Eq a,Ord a), puesto que Ord implica Eq. Además, los métodos de las subclases
pueden asumir la existencia de los métodos de la superclase. Por ejemplo, la declaración dee Ord en el
standar prelude incluye el siguiente método por defecto:
x < y = x <=y && x/=y
Haskell también permite la herencia múltiple, puesto que las clases pueden tener más de una
superclase. Los conflictos entre nombres se evitan mediante la restricción de que una operación
particular sólo puede ser miembro de una única clase en un ámbito determinado.
En el Standar Prelude se definen una serie de clases de tipos de propósito general.
Paradigmas de Programación
19
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
14. Evaluación Perezosa(Lazy)
Los lenguajes tradicionales, evalúan todos los argumentos de una función antes de conocer si éstos
serán utilizados. Dicha técnica de evaluación se conoce como evaluación ansiosa (eager evaluation)
porque evalúa todos los argumentos de una función antes de conocer si son necesarios.
Por otra parte, en ciertos lenguajes funcionales se utiliza evaluación perezosa (lazy evaluation) que
consiste en no evaluar un argumento hasta que no se necesita.
Haskell, Miranda y Clean son perezosos, mientras que LISP, SML, Erlang y Scheme son estrictos.
Uno de los beneficios de la evaluación perezosa consiste en la posibilidad de manipular estructuras de
datos 'infinitas'. Evidentemente, no es posible construir o almacenar un objeto infinito en su totalidad.
Sin embargo, gracias a la evaluación perezosa se puede construir objetos potencialmente infinitos
pieza a pieza según las necesidades de evaluación.
15. Ejercicios Prácticos
--Función recursiva para calcular el factorial de un número
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
--Función para calcular las raíces de una ecuación de segundo grado a partir de
sus coeficientes
raíces :: Float -> Float -> Float -> (Float, Float)
raíces a b c
| disc >= 0 = ((-b + raizDisc) / denom,
(-b - raizDisc) / denom)
| otherwise = error "La ecuación tiene raíces complejas"
where
disc = b*b - 4*a*c
raizDisc = sqrt disc
denom = 2*a
--Función para calcular el valor de e (2.71828182845905)
euler :: Double -> Double
euler 0.0 = 1.0
euler n = 1.0 / product [1..n] + euler (n - 1.0)
--Algoritmo de ordenación quicksort
qs::Ord a=>[a]->[a]
qs [] = []
qs (p:xs) = qs [x|x<-xs,x<=p] ++ [p] ++ qs [x|x<-xs,x>p]
ANEXO – La enseñanza de Haskell Paradigmas de Programación
20
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Haskell en la enseñanza de la Programación Declarativa(ProDec).
Desde el punto de vista educativo, un aspecto muy importante para la elección de un lenguaje es la
existencia de intérpretes y compiladores eficientes y de libre disposición.
Casi simultáneamente, junto a la aparición del primer compilador eficiente y completo para Haskell
desarrollado ´unicamente para plataformas UNIX, el primer sistema disponible para PCs surge a partir
de 1992 con el desarrollo de Gofer50 por Mark Jones en las universidades de Oxford, Yale y
Nottingham.
El éxito de un lenguaje es el disponer de un entorno de desarrollo adecuado. Muchos lenguajes no se
usan por carecer de tal entorno (p.e. Prolog).
Hugs 98 proporciona características adicionales que Haskell 98 no presenta en su definición original,
como la posibilidad de utilizar una librería de funciones gráficas, o ampliaciones del sistema de tipos
para describir datos vía registros extensibles.
Se puede afirmar existen motivos que aconsejan el uso de Haskell en favor de otro lenguaje funcional
muy utilizado en las universidades, como LisP, o también su derivado, Scheme.
Quizás se podría argumentar que LisP, además de ser considerado el primer lenguaje funcional, es uno
de los lenguajes más desarrollados, e incluso es aún un lenguaje vivo desde el punto de vista
educacional y sigue utilizándose en muchas universidades pero esencialmente para aplicaciones dentro
de la Inteligencia Artificial.
Sin embargo, ni LisP ni Scheme presentan la pureza ni algunas de las ventajas de Haskell. Entre éstas
se podrían destacar la descripción de ecuaciones vía patrones a la izquierda, el polimorfismo
restringido y el sistema de clases de tipos, que obligan a hacer un uso del lenguaje en forma
disciplinada.
Haskell es el lenguaje con más posibilidades entre los actualmente existentes, es de amplia aceptación
en muchas universidades, y está siendo utilizado en la industria cada vez con más profusión. Cabe
resaltar que la Universidad de Málaga es pionera en su uso para la docencia y la investigación, desde
la extensa divulgación realizada por la ACM
Los motivos citados justifican la elección del lenguaje Haskell, además justifican el uso de Haskell por
sobre la elección de otros lenguajes funcionales. El lenguaje utilizado en el paradigma funcional queda
reflejado en el título de muchos libros de programación funcional. Citemos por ejemplo [Bird, 1998]:
Introduction to Functional Programming using Haskell , una revisión y adaptación a Haskell del
clásico [Bird y Wadler, 1988]. O también, [Thompson, 1999], Haskell: The Craft of Functional
Programming, al igual que [Ruiz Jim´enez et al., 2000], Razonando con Haskell. O el último e
interesante libro de [Hudak, 2000], The Haskell School of Expression. Learning Functional
Programming through Multimedia.
Paradigmas de Programación
21
Universidad Tecnológica Nacional
Facultad Regional Córdoba
Ingeniería en Sistemas de Información
Cátedra de Paradigmas de Programación - 2011
Bibliografia & Papers :
- Introduction to Functional Programming using Haskell- Richard
Bird, Prentice Hall International, 2nd Ed. New York, 1997
- Basic polimorphics typecheking – Luca Cardelli
- A gentle introduction to Haskell
- The craft of Functional Programming – Simon Thompson
- Introducción al lenguaje Haskell - Jose E. Labra G.
- http://www.haskell.org
Paradigmas de Programación
22
Descargar