Haskell Hero

Haskell Hero es un manual interactivo del lenguaje Haskell para principiantes.

Tipos II

Creamos cajas nuevas

Como ya hemos visto en la lección Tipos I, Haskell dispone de una caja de todos números enteros, una caja de todos caracteres o por ejemplo una caja de dos valores booleanos. A veces es útil crear una caja nueva y en esta poner valores con que queremos trabajar.

Ejemplo

Cread un tipo que va a contener todos los cuadrados indicados por la longitud de su lado y todos los rectángulos indicados por las longitudes de su anchura y altura.


Vamos a llamar nuestro tipo por ejemplo Figura. Cuidado con la mayúscula 'F' inicial!

Por ejemplo un cuadrado con la longitud de lado 4 será Cuadrado 4, un cuadrado con la longitud de lado 10 será Cuadrado 10, etc. ¡Cuidado con la mayúscula 'C' inicial!

Un rectángulo de anchura 7 y altura 2 será Rectángulo 7 2, un rectángulo de anchura 1 y altura 6 será Rectángulo 1 6, etc.

En general queremos crear una caja Figura en la que todos valores están escritos como Cuadrado Integer o Rectángulo Integer Integer. Esto podemos definir de manera siguiente:

data Obrazec = Cuadrado Integer | Rectangulo Integer Integer
Nota: Si quisiéramos trabajar con figuras que no tienen dimensiones de números enteros, en vez de Integer usaríamos Float o Double.


Ahora sabemos como definir el tipo Figura que contiene todos los cuadrados definidos como Cuadrado Integer y todos los rectángulos definidos como Rectangulo Integer Integer donde podemos sustituir Integer por cualquier número entero.

Podríamos por ejemplo crear un tipo Diccionario definido de manera siguiente:

data Diccionario = Palabra String
Aquí podemos sustituir String por cualquiera cadena. Este tipo contiene por ejemplo valores:
Palabra "Hola"
Palabra "Haskell"
Palabra "&@#Đđ"

Trabajamos con cajas nuevas

Ahora cuando tenemos la caja de todos los cuadrados y rectángulos, podemos trabajar con ella de la misma manera como con otros tipos.

Ejemplo

Definid funciones perimetro y superficie que calcularán el perímetro y la superficie de valores de tipo Figura.


Primero tenemos que poner nuestra definición del tipo Figura.

data Figura = Cuadrado Integer | Rectangulo Integer Integer

Las dos funciones serán de tipo Figura -> Integer, entonces podemos escribir las anotaciones de tipo:

perimetro  :: Figura -> Integer
superficie :: Figura -> Integer

Ahora la definición propia. Todos sabemos que el perímetro de un cuadrado con la longitud de lado x es igual a 4 * x. Entonces lo escribimos:

perimetro             :: Figura -> Integer
perimetro (Cuadrado x) = 4 * x

¡Cuidado con los paréntesis! En la anotación de tipo indicamos que la función perimetro debe ser unaria, lo que significa que toma un parámetro. Si escribiéramos

perimetro Cuadrado x = 4 * x
Hugs identificaría el constructor Cuadrado y la variable x como dos parámetros para la función perimetro. La función perimetro puede, no obstante, tomar solo un valor de tipo Figura.

A esta definición podemos añadir también el perímetro de rectángulo. Sabemos que el perímetro de rectángulo de anchura x y altura y es igual a 2 * (x + y).

perimetro                 :: Figura -> Integer
perimetro (Cuadrado x)     = 4 * x
perimetro (Rectangulo x y) = 2 * (x + y)

Análogamente definimos la función superficie:

superficie                 :: Figura -> Integer
superficie (Cuadrado x)     = x ^ 2
superficie (Rectangulo x y) = x * y

Cuando tenemos las funciones definidas y preguntamos a Hugs qué es el perímetro del rectángulo 5 x 3, nos dice correctamente que 16.

   perimetro (Rectangulo 5 3)
~> 2 * (5 + 3)
~> 2 * 8
~> 16

Tipos recursivos

Ahora ya sabemos como definir un tipo simple. Sin embargo, algunas veces vamos a necesitar un tipo más complejo. Por ejemplo quisiéramos crear un tipo Nat que representa números naturales con cero como Zero, lo que es cero, o Succ x, lo que es el sucesor (de la palabra inglés succesor) del número x donde x es de tipo Nat. En otras palabras, quisiéramos crear una caja que contenga valores siguientes:

Zero
  -- cero
Succ Zero
  -- el sucesor de cero - dos
Succ (Succ Zero)
  -- el sucesor del sucesor de cero - dos
Succ (Succ (Succ Zero))
  -- el sucesor del sucesor del sucesor de cero - tres
...
¿Cómo definir un tal tipo? Tenemos que describir todos sus valores. En el ejemplo del párrafo anterior los valores de tipo Figura fueron o Cuadrado Integer o Rectangulo Integer Integer donde Integer se podía sustituir por cualquier número entero. Aquí tenemos dos formas:

  • Zero
  • Succ Nat

DondeNat se puede sustituir de manera recursiva por cualquier valor de tipo Nat. El tipo vpodemos entonces definir de manera siguiente:

data Nat  =  Zero  |  Succ Nat
Un tipo definido de esta manera solamente indica la estructura del almacenamiento de datos en la memoria. No se dice en ningún lugar como imprimir los valores si es necesario. La manera más fácil como hacerlo es añadir la cadena deriving Show a la definición de tipo. ¡Cuidado con la minúscula 'd' y la mayúscula 'S'! La definición de tipo Nat con la posibilidad de imprimir los valores será la siguiente:
data Nat  =  Zero  |  Succ Nat  deriving Show

Funciones con tipos recursivos

Aquí vamos a definir una función simple con el tipo Nat del párrafo anterior. Esta función está comparada con la adición de manzanas para que se comprenda mejor.

Ejemplo

Manzanas Nat
Definid la función masManzanas que toma dos recipientes de manzanas como parámetros y nos devuelve un recipiente donde habrá tantas manzanas como en las dos recipientes anteriores en total. Definid la función masNat :: Nat -> Nat -> Nat que sumará dos números de tipo Nat.

¿Cómo empezar? Tenemos que acordarnos de que podemos hacer con los valores de tipo Nat según nuestra definición. Podemos solo identificar si el valor es en forma Zero o en forma Succ (...). Nada más. Esto significa que la función masNat puede tomar como el primer parámetro o Zero o Succ (...). ¿Cómo definir la función para estos casos particulares?

El primer parámetro es un recipiente vacío. Podemos decir que en ambos recipientes hay tantas manzanas como en el segundo recipiente. El resultado es entonces el segundo recipiente. Evaluamos la expresión masNat Zero y. Queremos que el resultado de la adición del valor Zero y el valor y sea el valor y. La expresión la entonces evaluamos a y.
Manzanas: El primer parámetro es un recipiente que contiene al menos una manzana. Tiramos la manzana al segundo recipiente y probamos si el primer recipiente ya está vacío. Evaluamos la expresión masNat (Succ x) y. La evaluamos a la expresión plusNat x (Succ y), con lo que disminuimos el primer número por uno y el segundo número lo aumentamos por uno.
Ya que en el primer recipiente hay solo un número finito de manzanas, sabemos que el recipiente se vaciará totalmente con el tiempo. Ya que el número en el primer parámetro es finito y durante cada evaluación se disminuye por uno, llegaremos al momento cuando se disminuye a cero.
emos tirado todas las manzanas al segundo recipiente y como el resultado devolvemos el segundo recipiente. Disminuyendo el primer parámetro y aumentando el segundo parámetro poco a poco el primer parámetro se disminuye a cero y en este momento podemos evaluar la expresión al segundo parámetro.

Podemos entonces definir la función masNat de manera siguiente:

masNat            ::  Nat -> Nat -> Nat
masNat  Zero    y  =  y
masNat (Succ x) y  =  masNat x (Succ y)