You are on page 1of 7

Laboratorio Análisis Lógico

Práctica 2: Tipos de datos en Haskell


Pedro A. Góngora Luna1

1. Operadores
La figura 1 muestra la lista de algunos de los operadores básicos definidos en el preludio. La lista
está en orden de precedencia, esto es, los primeros elementos ejercen “mayor atracción” a sus operandos
que los últimos.

Operador Descripción
Aplicación de funciones
!! n–ésimo elemento de una lista
. Composición de funciones
**, ^, ^^ Exponenciación
*, /, ‘div‘, ‘quot‘, ‘mod‘, ‘rem‘ Multiplicación, división y residuo
+, - Suma y resta
:, ++ Inserción y concatenación en listas
==, \=, <, <=, >, >= Operadores relacionales
&& “and” booleano
|| “or” booleano
>>, >>= Composición secuencial de acciones
$ Aplicación

Figura 1: Operadores en orden de precedencia

Conviene resaltar que los operadores encerrados entre un par de ‘‘’ son la versión infija de las
funciones con el mismo nombre2 . En Haskell podemos convertir cualquier función binaria a infija
encerrándola entre dos caracteres ‘‘’.
De manera similar en que podemos convertir funciones binarias a operadores infijos, también po-
demos convertir operadores infijos a funciones prefijas encerrándolos entre paréntesis. Por ejemplo

Prelude> (+) 10 10
20
Prelude> (&&) True False
False

La expresión f $ x significa “aplica la función f al operando x”. En el preludio, podrı́amos encontrar


la definición de $ como

f $ x = (f x)

Esta definición parece redundante, sin embargo, puede ser útil para evitar un exceso de paréntesis.
Por ejemplo, en lugar de escribir

i (h (g (f x)))

1 pedro.gongora@gmail.com
2 El caracter ‘`’ es el acento grave, diferente de ‘’’ y ‘´’, que son un simple apóstrofo y el acento agudo (el que usamos

en español).

1
podemos escribir

i $ h $ g $ f x

El operador $ también puede ayudar con funciones de orden superior. Por ejemplo

Prelude> map ($10) [(1+),(1*),(1/)]


[11.0,10.0,0.1]

Los operadores >> y >>= son fundamentales para la entrada/salida en Haskell. Más adelante se
estudiarán.

2. Clases de tipos y polimorfismo


2.1. Tipos de datos de expresiones
Para averiguar la “firma” o tipo de dato de datos de alguna expresión, podemos usar el comando
:t del intérprete. Veamos como ejemplo el tipo de la función not (negación lógica):

Prelude> :t not
not :: Bool -> Bool

El resultado not :: Bool -> Bool, debe leerse como “not es una función que va de booleanos a
booleanos”. En matemáticas escribirı́amos

not : B → B

Veamos ahora el tipo de la función binaria && (conjunción lógica):

Prelude> :t (&&)
(&&) :: Bool -> Bool -> Bool

Hay que recordar que en Haskell, las funciones están “curryficadas”, por lo que la función && no es
una función que va de parejas de booleanos a booleanos. En realidad, && es una función que recibe un
valor booleano y devuleve una función que, a su vez, recibe otro booleano y regresa otro booleano.
Parece complejo, pero vayamos por partes. La aplicación de funciones se denota con un espacio,
por ejemplo, la expresión

f x

quiere decir que aplicamos la función f al argumento x.


La aplicación de funciones es asociativa a la izquierda. Por ejemplo, una expresión como

(&&) True False

en realidad debe leerse como

((&&) True) False

donde ((&&) True) es una función que devuelve True sólo si su operando también es True. De hecho,
si examinamos el tipo de ((&&) True), vermos lo siguiente:

2
Prelude> :t ((&&) True)
(True &&) :: Bool -> Bool

En Haskell también podemos crear funciones “normales” usando tuplas. Por ejemplo, defininamos
la función

and2 (x,y) = x && y

Al examinar el tipo de and2 obtenemos

Main> :t and2
and2 :: (Bool,Bool) -> Bool

Otra definición alternativa es utilizando la función uncurry

and2 = uncurry (&&)

Es conveniente resaltar que las tuplas, a diferencia de las listas, son heterogeneas, es decir, los
elementos de una tupla pueden ser de tipos distintos.

2.2. Funciones polimórficas


Podemos escribir funciones de forma genérica, sin especificar el tipo de datos a usar. Por ejemplo,
si preguntamos al intérprete por el tipo de la función length, que calcula la longitud de una lista,
obtenemos lo siguiente:

Prelude> :t length
length :: [a] -> Int

En este caso, a es una variable de tipos. Esto quiere decir que podemos sustituir a por cualquiera de
los tipos de datos disponibles. De esta manera podemos tener una sola función que calcule la longitud
de una lista de elementos de cualquier tipo.

2.3. Clases
Al revisar la figura 1, seguramente surge una pregunta: ¿por qué hay varias versiones para ope-
radores similares? Por ejemplo, intuitivamente, / y ‘div‘ parece que hacen lo mismo. En realidad,
trabajan con tipos de datos diferentes.

Prelude> :t (/)
(/) :: (Fractional a) => a -> a -> a
Prelude> :t (div)
(div) :: (Integral a) => a -> a -> a

En la firma de la función div, observamos que se refiere a un tipo de dato Integral a. Esto quiere
decir que a es una instancia de la clase Integral.
En Haskell los tipos de datos pueden derivarse de otros tipos de datos. Por ejemplo Int e Integer
son instancias de Integral (Integer nos sirve para representar enteros no acotados). La figura 2 tiene
algunas de las clases básicas definidas en el preludio.
Para obtener más información sobre algun tipo o clase, podemos usar el comando :i

3
Prelude> :i Int
-- type constructor
data Int
-- instances:
instance Eq Int
instance Ord Int
instance Num Int
instance Bounded Int
instance Real Int
instance Integral Int
instance Ix Int
instance Enum Int
instance Read Int
instance Show Int

Otros tipos de datos básicos son Char, Double, Float y Bool. El tipo [Char] es para listas de
caracteres, mientras que un tipo [a] es para listas de algún tipo a. String es un sinónimo de [Char].

2.4. Restringiendo el tipo de una función


Recordemos la definición de la función sumList

sumList [] = 0
sumList (h:t) = h + sumList t

Si examinamos su tipo veremos que Haskell infiere, por el uso del operador +, que es una función
que trabaja con cualquier instancia de la clase Num.
Podemos restringir nuestra función a listas de enteros de la siguiente forma

sumList :: [Int] -> Int


sumList [] = 0
sumList (h:t) = h + sumList t

Incluso, podemos ser más generales y restringirla a cualquier instancia de la clase Integral

sumList :: Integral a => [a] -> a


sumList [] = 0
sumList (h:t) = h + sumList t

Clase Descripción Funciones


Eq Tipos con igualdad ==, \=
Ord Tipos con orden total <, <=, >, >=
Enum Tipos que pueden enumerarse succ, pred, enumFrom, enumFromTo
Show Conversión a cadena show
Read Conversión desde cadena read

Figura 2: Algunas clases básicas

4
3. Tipos algebráicos
3.1. Sinónimos
En Haskell podemos crear nuestros propios tipos de datos. Por ejemplo, el tipo String mencionado
anteriormente está creado de la siguiente forma:

type String = [Char]

La palabra type crea sinónimos para tipos de datos existentes. Podemos crear nombres más con-
venientes para los tipos disponibles

type Nombre = String


type Edad = Int
type Salario = Float

3.2. Tipos enumerados


Usando la palabra data podemos crear nuevos tipos enumerando sus componentes. Por ejemplo

data Planeta = Mercurio | Venus | Tierra | Marte |


Jupiter | Saturno | Urano | Neptuno

Podemos definir funciones sobre nuestro nuevo tipo

habitable :: Planeta -> Bool


habitable Tierra = True
habitable _ = False

El carácter _ es un comodı́n o wildcard. En este contexto, significa que cualquier planeta, que no
sea la Tierra, no es habitable (recordemos que la concordancia o pattern–matching se hace en el orden
de como se escribieron los patrones).
Podemos hacer que Haskell construya automáticamente algunas funciones por nosotros indicándole
que derive de clases ya definidas. Por ejemplo,

data Planeta = Mercurio | Venus | Tierra | Marte |


Jupiter | Saturno | Urano | Neptuno
deriving (Eq, Ord, Show, Enum)

Esta nueva definición de Planeta permite usar algunas funciones ya establecidas para las clases
Eq, Ord, Show y Enum.

Main> Mercurio == Tierra


False
Main> Mercurio < Tierra
True
Main> show Mercurio
"Mercurio"
Main> [(Mercurio)..(Tierra)]
[Mercurio,Venus,Tierra]
Main> map habitable [(Mercurio)..(Neptuno)]
[False,False,True,False,False,False,False,False]

5
3.3. Tipos producto
Con data también podemos crear tipos más complejos, por ejemplo

type Nombre = String


type Salario = Float
type Empresa = String
data Empleado = EmpInterno Nombre Salario | EmpExterno Nombre Empresa

Además de definir funciones sobre estos nuevos tipos:

showEmpleado :: Empleado -> String


showEmpleado (EmpInterno n s) = n ++ " gana $" ++ (show s)
showEmpleado (EmpExterno n e) = n ++ " es de la empresa " ++ e

Ası́ que podemos obtener cosas como:

Main> showEmpleado (EmpInterno "Juan" 10000.0)


"Juan gana $10000.0"
Main> showEmpleado (EmpExterno "Luis" "OCC")
"Luis es de la empresa OCC"

En este caso, EmpInterno y EmpExterno se llaman “constructores”. Podemos leer las definicio-
nes como “Un empleado es interno o externo”. “Un empleado interno tiene nombre y salario”. “Un
empleado externo tiene nombre y una empresa que lo contrata”.
En el caso de los planetas, Mercurio, etc., decimos que son constructores con aridad 0 (nullary
constructors).
Hay que tomar en cuenta que los nombres de los tipos de datos y los constructores, deben comenzar
con una letra mayúscula.

3.4. Tipos recursivos


Por último, data nos permite crear tipos con definiciones recursivas.

data AExp = Lit Int | Add AExp AExp | Mul AExp AExp

La definición de AExp tiene dos componentes. Lit Int es el “conjunto base”, es decir, el conjunto
de las literales enteras, mientras que a Add y Mul, podemos verlas como las “funciones constructoras”.
Ası́ como lo hacemos en matemáticas, podemos definir funciones recursivas para nuestro conjunto
inductivo.
Definimos una función eval para los elementos del conjunto base:

eval :: AExp -> Int


eval (Lit x) = x

Definimos una función recursiva evalrec para el resto de los elementos:

evalrec :: AExp -> Int


evalrec (Lit x) = eval (Lit x)
evalrec (Add x y) = (evalrec x) + (evalrec y)
evalrec (Mul x y) = (evalrec x) * (evalrec y)

6
Ahora que nuestro tipo de datos tiene significado, podemos evaluar algunas expresiones:

Main> evalrec (Add (Lit 12) (Mul (Lit 34) (Lit 1)))
46

También podemos usar los constructores en versión infija:

Main> evalrec ((Lit 20) ‘Add‘ (Lit 50))


70

4. Ejercicios
1. Defina un tipo de dato recursivo para las expresiones booleanas con los siguientes elementos:
a) Las constantes booleanas V y F,
b) Variables de un carácter que representen a las proposiciones atómicas,
c) Los conectivos lógicos Not, And, Or, IfThen, Iff.

2. Defina una función de evaluación para los elementos del conjunto básico (para las proposiciones
atómicas sólo defina unos cuantos ejemplos).
3. Defina una función de evaluación recursiva para el resto de los elementos.

Entrega: Por correo electrónico a pedro.gongora@gmail.com, fecha por definir.

You might also like