Una Introducción agradable a Haskell
anterior siguiente inicio


2  Valores, Tipos, y otras Golosinas 

Puesto que Haskell es un lenguaje funcional puro, todos los cómputos vienen descritos a través de la evaluación de expresiones (términos sintácticos) para producir valores (entidades abstractas que son vistas como respuestas). Todo valor tiene asociado un tipo. (Intuitivamente, podemos pensar que los tipos son conjuntos de valores.) Ejemplos de expresiones son los valores atómicos tales como el entero 5, o el carácter 'a', o la función \x -> x+1, y los valores estructurados como la lista [1,2,3] y el par ('b',4).

Ya que las expresiones denotan valores, las expresiones de tipo son términos sintácticos que denotan tipos. Ejemplos de expresiones de tipo son los tipos atómicos Integer (enteros con precisión ilimitada), Char (caracteres), Integer->Integer (funciones que aplican Integer sobre Integer), así como los tipos estructurados [Integer] (lista homogénea de enteros) y (Char,Integer) (par formado por un carácter y un entero).

Todos los valores de Haskell son de primera categoría ("first-class") ---pueden ser argumentos o resultados de funciones, o pueden ser ubicados en estructuras de datos, etc. Por otro lado, los tipos de Haskell no son de primera categoría. En cierto sentido, los tipos describen valores, y la asociación de un valor con su tipo se llama un tipificado (typing). Usando los ejemplos anteriores, podemos escribir "tipificaciones" como los siguientes:

                          5  :: Integer
                         'a' :: Char
                         inc :: Integer -> Integer
                     [1,2,3] :: [Integer]
                     ('b',4) :: (Char,Integer)

El símbolo "::" puede leerse "tiene el tipo".

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 (§4.4.1), 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

Veremos más detalles sobre definiciones de funciones en la Sección 3.

Por razones pedagógicas, cuando queramos indicar que una expresión e1 se evalúa, o "reduce" a otra expresión o valor e2, escribiremos:

e1 => e2

Por ejemplo:

inc (inc 3) => 5

El sistema de tipificación estático de Haskell define formalmente la relación entre tipos y valores (§4.1.4). Esta tipificación estática asegura que un programa Haskell está bien tipificado  (type safe); es decir, que el programador no puede evaluar expresiones con tipos erróneos. Por ejemplo, no podemos sumar dos caracteres, ya que la expresión 'a'+'b' está mal tipificada. La ventaja principal del tipificación estática es bien conocida: todos los errores de tipificado son detectados durante la compilación. No todos los errores son debidos al sistema de tipos; una expresión tal como 1/0 es tipificable pero su evaluación provoca un error en tiempo de ejecución. No obstante, el sistema de tipos puede encontrar errores durante la compilación, lo que proporciona al programador una ayuda para razonar sobre los programas, y también permite al compilador generar un código más eficiente (por ejemplo, no se requiere ninguna información de tipo o pruebas durante la ejecución).

El sistema de tipos también asegura que los tipos que el usuario proporciona para las funciones son correctos. De hecho, el sistema de tipos de Haskell es lo suficientemente potente como para describir cualquier tipo de función (con algunas excepciones que veremos más tarde) en cuyos caso diremos que el sistema de tipos infiere tipos correctos. No obstante, son aconsejables las oportunas declaraciones de tipos para las funciones, como la proporcionada para la función inc, ya que el tipificado de funciones es una forma eficiente de documentar y ayudar al programador a detectar errores.

[El lector habrá notado que los identificadores que comienzan con mayúscula denotan tipos específicos, tales como Integer y Char, pero no los identificadores que denotan valores, como inc. Esto no es un convenio: es obligatorio debido a la sintaxis de Haskell. Además, todos los caracteres, mayúsculas y minúsculas, son significativos: foo, fOo, y fOO son identificadores distintos.]

2.1  Tipos Polimórficos

Haskell proporciona tipos polimóficos ---tipos que son 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'.)

[Los identificadores tales como el anterior se llaman variables de tipo, y se escriben en minúscula para distinguirlas de tipos específicos, como Integer. Además, 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:[].

Como ejemplo de función definida por el usuario y que opera sobre listas, consideremos el problema de contar el número de elementos de una lista:

length                  :: [a] -> Integer
length []               =  0
length (x:xs)           =  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).

Este ejemplo, además de intuitivo, enfatiza un aspecto importante de Haskell que debemos aclarar: la comparación de patrones (pattern matching). 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.

La definición de funciones a través de comparación de patrones es usual en Haskell, y el usuario deberá familiarizarse con los distintos tipos de patrones que se permiten; volveremos a esta cuestión en la Sección 4.

La función length es también un ejemplo de función polimórfica. Puede aplicarse a listas con elementos de cualquier tipo, por ejemplo [Integer], [Char], o [[Integer]].

length [1,2,3] => 3
length ['a','b','c'] => 3
length [[1],[2],[3]] => 3

He aquí dos funciones polimórficas muy útiles sobre listas, que usaremos más tarde. La función head devuelve el primer elemento de una lista, y la función tail devuelve la lista salvo el primero:

head                    :: [a] -> a
head (x:xs)             =  x

tail                    :: [a] -> [a]
tail (x:xs)             =  xs

Al contrario que length, estas funciones no estan definidas para todos los posibles valores de su argumento. Cuando las funciones son aplicadas a la lista vacía se produce un error en tiempo de ejecución.

Vemos que algunos tipos polimórficos son más generales que otros en el sentido de que el conjunto de valores que definen es más grande. Por ejemplo, el tipo [a] es más general que [Char]. En otras palabras: el tipo [Char] puede ser derivado del tipo [a] a través de una sustitución adecuada de a. Con respecto a este orden generalizado, el sistema de tipos de Haskell tiene dos propiedades importantes: en primer lugar, se garantiza que toda expresión bien tipificada tenga un único tipo principal (descrito después), y en segundo lugar, el tipo principal puede ser inferido automáticamente (§4.1.4). En comparación con un lenguaje con tipos monomórficos como C, el lector encontrará que el polimórfismo enriquece la expresividad, y que la inferencia de tipos reduce la cantidad de tipos usados por el programador.

El tipo principal de una expresión o función es el tipo más general que, intuitivamente, "contiene todos los ejemplares de la expresión." Por ejemplo, el tipo principal de head es [a]->a; los tipos [b]->a, a->a, o el propio a son demasiado generales, mientras que algo como [Integer]->Integer es demasiado concreto. La existencia de un único tipo principal es la característica esencial del sistema de tipos de Hindley-Milner, que es la base del sistema de tipos de Haskell, ML, Miranda, ("Miranda" es marca registrada de Research Software, Ltd.) y otros lenguajes (principalmente funcionales) .

2.2  Tipos definidos por el usuario

Podemos definir nuestros propios tipos en Haskell a través de una declaración data, que introduciremos con una serie de ejemplos (§4.2.1).

Un dato predefinido importante en Haskell corresponde a los valores de verdad:

data Bool               = False | True

El tipo definido con tal declaración es Bool, y tiene exactamente dos valores: True y False. Bool es un ejemplo de constructor de tipo (sin argumentos), mientras que True y False son constructores de datos (o constructores, para abreviar).

En forma similar, podemos definir un tipo color:

data Color              = Red | Green | Blue | Indigo | Violet

Tanto Bool como Color son ejemplos de tipos enumerados, puesto que constan de un número finito de constructores.

El siguiente es un ejemplo de tipo con un solo constructor de dato:

data Point a            = Pt a a

Al tener un solo constructor, un tipo como Point es llamado a menudo un tipo tupla, ya que esencialmente es un producto cartesiano (en este caso binario) de otros tipos. (La tuplas son conocidas en otros lenguajes como registros.) Por el contrario, un tipo multi-constructor, tal como Bool o Color, se llama una "suma de tipos" o tipo unión (disjunta).

Sin embargo, lo más importante es que Point es un ejemplo de tipo polimórfico: para cualquier tipo t, define el tipo de los puntos cartesianos que usan t como eje de coordenadas. El tipo Point puede también verse como un constructor de tipos unario, ya que a partir de un tipo t podemos obtener un nuevo tipo Point t. (En el mismo sentido, usando el ejemplo de la listas, [] es también un constructor de tipos: podemos aplicar el constructor [] a un tipo t para obtener un nuevo tipo [t]. La sintaxis de Haskell permite escribir [t] en lugar de [] t. Similarmente, -> es otro constructor de tipos binario: dados dos tipos "t" y "u", t->u es el tipo de las funciones que aplican datos de tipo "t" a elementos de tipo "u".)

Nótese que el tipo del constructor de datos Pt es a -> a -> Point a, y las siguientes asignaciones de tipos son válidas:

Pt  2.0  3.0            :: Point Float
Pt  'a'  'b'            :: Point Char
Pt True False           :: Point Bool

Por otro lado, una expresión tal como Pt 'a' 1 está erróneamente tipificada, ya que 'a' y 1 son de tipos diferentes.

Es importante distinguir entre la aplicación de un constructor de datos para obtener un valor, y la aplicación de un constructor de tipos para obtener un tipo; el primero tiene lugar durante el tiempo de ejecución, que es cuando se computan cosas en Haskell, mientras que el último tiene lugar en tiempo de compilación y forma parte del proceso de tipificado que asegura un "tipo seguro".

[Constructores de tipo como Point y constructores de datos como Pt aparecen en niveles distintos de la declaración, lo que permite que el mismo nombre pueda usarse como constructor de tipos y como constructor de datos, como vemos en:

data Point a = Point a a

Esto puede llevar a una pequeña confusión al principio, pero sirve para crear un enlace obvio entre el constructor de datos y el de tipo.]

2.2.1  Tipos recursivos

Los tipos pueden ser recursivos, como el siguiente tipo para árboles binarios:

data Tree a             = Leaf a | Branch (Tree a) (Tree a) 

Con ello hemos definido un tipo polimórfico cuyos elementos son o bien hojas conteniendo un valor de tipo a, o nodos internos ("ramas") conteniendo (en forma recursiva) dos subárboles.

En la lectura de declaraciones de datos como la anterior, recordemos que Tree es un constructor de tipos, mientras que Branch y Leaf son constructores de datos. La declaración anterior, además de establecer una conexión entre estos constructores, define esencialmente los tipos para los constructores Branch y Leaf:

Branch                  :: Tree a -> Tree a -> Tree a
Leaf                    :: a -> Tree a

Con este ejemplo tenemos un tipo suficientemente rico que permite definir algunas funciones (recursivas) interesantes que hagan uso de éste. Por ejemplo, supongamos que queremos definir una función fringe que devuelva todos los elementos de las hojas de un árbol de izquierda a derecha. En primer lugar es esencial escribir el tipo de la nueva función; en este caso vemos que el tipo debe ser Tree a -> [a]. Es decir, fringe es una función polimórfica que, para cualquier tipo a, aplica árboles de a sobre listas de a. Una definición adecuada es la siguiente:

fringe                     :: Tree a -> [a]
fringe (Leaf x)            =  [x]
fringe (Branch left right) =  fringe left ++ fringe right

donde ++ es el operador infijo que concatena dos listas (su definición completa se verá en la Section 9.1). Al igual que la función length vista anteriormente, la función fringe está definida mediante comparación de patrones, salvo que los patrones implicados son los constructores de la definición dada por el usuario: Leaf y Branch. [Nótese que los parámetros formales son fácilmente identificados ya que comienzan con letras minúsculas.]

2.3  Sinónimos de Tipos

Por conveniencia, Haskell proporciona una forma para definir sinónimos de tipos; es decir, nombres de tipos usados varias veces. Los sinónimos de tipo son creados a través de una declaración type (§4.2.2). He aquí algunos ejemplos:

type String             = [Char]
type Person             = (Name,Address)
type Name               = String
data Address            = None | Addr String

Los sinónimos no definen tipos nuevos, sino simplemente proporcionan nuevos nombres a tipos ya existentes. Por ejemplo, el tipo Person -> Name es precisamente equivalente al tipo (String,Address) -> String. Los nombres nuevos son a menudo más cortos que los tipos nombrados, pero éste no es el único propósito de los sinónimos de tipos: éstos pueden mejorar la legibilidad de los programas a través de nemotécnicos; en efecto, los ejemplos anteriores enfatizan este hecho. Podemos dar nuevos nombres a tipos polimórficos:

type AssocList a b              = [(a,b)]

Este es el tipo de las "listas de asociaciones" que asocian valores de tipo a con otros de tipo b.

2.4  Los tipos predefinidos no son especiales

Antes hemos introducido varios tipos "predefinidos" tales como listas, tuplas, enteros y caracteres. También mostramos como el programador puede definir nuevos tipos. Además de una sintaxis especial ¿los tipos predefinidos tienen algo más de especial? La respuesta es no. La sintaxis especial es por conveniencia y consistencia, junto a algunas razones históricas, pero no tiene ninguna consecuencia semántica.

Enfatizamos este punto diciendo que la apariencia de las declaraciones de éstos tipos predefinidos es especial. Por ejemplo, el tipo Char puede ser descrito en la forma:

data Char       = 'a' | 'b' | 'c' | ...         -- Esto no es código Haskell
                | 'A' | 'B' | 'C' | ...         -- válido!
                | '1' | '2' | '3' | ...
                ...

Los nombres de los constructores no son válidos sintácticamente; ello lo podríamos arreglar escribiendo algo como lo siguiente:

data Char       = Ca | Cb | Cc | ...
                | CA | CB | CC | ...
                | C1 | C2 | C3 | ...
                ...

Tales constructores son más concisos, pero no son los habituales para representar caracteres.

En cualquier caso, la escritura de código "pseudo-Haskell" tal como la anterior ayuda a aclarar la sintaxis especial. Vemos que Char es, en efecto, un tipo enumerado compuesto de un gran número de constructores (constantes). Por ejemplo, visto Char de esta forma aclaramos qué patrones pueden aparecer en las definiciones de funciones; es decir, qué constructores de este tipo podemos encontrarnos.

Este ejemplo también muestra el uso de los comentarios en Haskell; los caracteres -- y los sucesivos hasta el final de la línea son ignorados. Haskell también permite comentarios anidados que tienen las forma {-...-} y pueden aparecer en cualquier lugar (§2.2).]

Similarmente, podemos definir Int (enteros de precisión limitada) y Integer en la forma:

data Int     = -65532 | ... | -1 | 0 | 1 | ... | 65532  -- más pseudo-código
data Integer =       ... -2 | -1 | 0 | 1 | 2 ...

donde -65532 y 65532, representan el mayor y el menor entero en precisión fija para una implementación concreta. Int es un tipo enumerado más largo que Char, pero es finito! Por el contrario, el pseudo-código para Integer (el tipo de los enteros con precisión arbitraria) debe verse como un tipo enumerado infinito.

Las tuplas también son fáciles de definir en la misma forma:

data (a,b)              = (a,b)                         -- más peudo-código
data (a,b,c)            = (a,b,c)
data (a,b,c,d)          = (a,b,c,d)
 .                         .
 .                         .
 .                         .

Cada una de las declaraciones anteriores define una tupla de una longitud particular, donde (...) juega distintos papeles: a la izquierda como constructor de tipo, y a la derecha como constructor de dato. Los puntos verticales después de la última declaración indican un número infinito de tales declaraciones, reflejando el hecho de que en Haskell están permitidas las tuplas de cualquier longitud.

La listas son manipulables fácilmente, y lo que es más importante, son recursivas:

data [a]               = [] | a : [a]                  -- más peudo-código

Vemos que esto se ajusta a lo ya dicho sobre listas: [] es la lista vacía, y : es el constructor infijo de listas; de esta forma [1,2,3] es equivalente a la lista 1:2:3:[]. (: es asociativo a la derecha.) El tipo de [] es [a], y el tipo de : es a->[a]->[a].

[De esta forma ":" está definido con una sintaxis legal---los constructores infijos se permiten en declaraciones data, y (para describir la comparación de patrones) son distinguidos de los operadores infijos ya que comienzan con el carácter ":" (una propiedad satisfecha trivialmente por ":").]

En este punto, el lector deberá notar con cuidado las diferencias entre tuplas y listas, ya que las definiciones anteriores lo aclaran suficientemente. En particular, nótese la naturaleza recursiva de las listas, con longitud arbitraria y cuyos elementos son homogéneos, y la naturaleza no recursiva de una tupla concreta, que tiene una longitud fija, en la cual los elementos son heterogéneos. Las reglas de tipificado para tuplas y listas deberían quedar claras ahora:

Para (e1,e2,...,en), n>=2, si ti es el tipo de ei, entonces el tipo de la tupla es (t1,t2,...,tn).

Para [e1,e2,...,en], n>=0, cada ei debe tener el mismo tipo t, y el tipo de la lista es [t].

2.4.1  Listas por comprensión y Secuencias Aritméticas

Como en algunos dialectos de Lisp, las listas son muy útiles en Haskell, y al igual que en otros lenguajes funcionales, existe aún una sintaxis más adecuada para su descripción. Además de los constructores de listas ya introducidos, Haskell proporciona expresiones conocidas como listas por comprensión que introducimos con un ejemplo:

[ f x | x <- xs ]

Intuitivamente, esta expresión puede leerse como "la lista de todos los f x tales que x recorre xs." La similitud con la notación de los conjuntos no es una coincidencia. La frase x <- xs se llama un generador, y pueden utilizarse varios, como en :

[ (x,y) | x <- xs, y <- ys ]

Tal lista por comprensión determina el producto cartesiano de dos listas xs y ys. Los elementos son seleccionados como si los generadores fueran anidados de izquierda a derecha (con el de más a la derecha variando el último); es decir, si xs es [1,2] e ys es [3,4], el resultado es [(1,3),(1,4),(2,3),(2,4)].

Además de los generadores, se permiten expresiones booleanas llamadas guardas que establecen restricciones sobre los elementos generados. Por ejemplo, he aquí una definición compacta del algoritmo de ordenación favorito de todo el mundo:

quicksort  []           =  []
quicksort (x:xs)        =  quicksort [y | y <- xs, y<x ]
                        ++ [x]
                        ++ quicksort [y | y <- xs, y>=x]

Como otra ayuda en la descripción de listas, Haskell admite una sintaxis especial para secuencias aritméticas, que mostramos con una serie de ejemplos:

[1..10] => [1,2,3,4,5,6,7,8,9,10]
[1,3..10] => [1,3,5,7,9]
[1,3..] => [1,3,5,7,9, ... (secuencia infinita)

Veremos algo más sobre secuencias aritméticas en la Sección 8.2, y sobre "listas infinitas" en la Sección 3.4.

2.4.2  Cadenas

Como otro ejemplo de sintaxis especial para tipos predefinidos, hacemos notar que la cadena de caracteres "hello" es una forma simplificada de la lista de caracteress ['h','e','l','l','o']. Además, el tipo de "hello" es String, donde String es un sinónimo de tipo predefinido:

type String             = [Char]

Esto significa que podemos usar las funciones polimórficas sobre listas para operar con cadenas (strings). Por ejemplo:

"hello" ++ " world" => "hello world"


Una Introducción agradable a Haskell
anterior siguiente inicio