Empezando A Programar Haskell
Empezando A Programar Haskell
Muy bien ¡Vamos a empezar! Si eres esa clase de mala persona que no lee las
introducciones y te la has saltado, quizás debas leer la última sección de la introducción
porque explica lo que necesitas para seguir esta guía y como vamos a trabajar. Lo primero
que vamos a hacer es ejecutar GHC en modo interactivo y utilizar algunas funciones para ir
acostumbrándonos un poco. Abre una terminal y escribe ghci. Serás recibido con un saludo
como éste:
GHCi, version 7.2.1: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude>
ghci> 2 + 15
17
ghci> 49 * 100
4900
ghci> 1892 - 1472
420
ghci> 5 / 2
2.5
ghci>
Se explica por si solo. También podemos utilizar varias operaciones en una misma línea
de forma que se sigan todas las reglas de precedencia que todos conocemos. Podemos usar
paréntesis para utilizar una precedencia explícita.
¿Muy interesante, eh? Sí, se que no, pero ten paciencia. Una pequeña dificultad a tener
en cuenta ocurre cuando negamos números, siempre será mejor rodear los números
negativos con paréntesis. Hacer algo como 5 * -3 hará que GHCi se enfade, sin
embargo 5 * (-3) funcionará.
La álgebra booleana es también bastante simple. Como seguramente
sabrás, && representa el Y lógico mientras que || representa
el O lógico. not niega True a False y viceversa.
ghci> True && False
False
ghci> True && True
True
ghci> False || True
True
ghci> not False
True
ghci> not (True && True)
False
ghci> 5 == 5
True
ghci> 1 == 0
False
ghci> 5 /= 5
False
ghci> 5 /= 4
True
ghci> "hola" == "hola"
True
¿Qué pasa si hacemos algo como 5 + "texto" o 5 == True? Bueno, si probamos con
el primero obtenemos este amigable mensaje de error:
No instance for (Num [Char])
arising from a use of `+' at <interactive>:1:0-9
Possible fix: add an instance declaration for (Num [Char])
In the expression: 5 + "texto"
In the definition of `it': it = 5 + "texto"
GHCi nos está diciendo es que "texto" no es un número y por lo tanto no sabe como
sumarlo a 5. Incluso si en lugar de "texto" fuera "cuatro", "four", o "4", Haskell no lo
consideraría como un número. + espera que su parte izquierda y derecha sean números. Si
intentamos realizar True == 5, GHCi nos diría que los tipos no coinciden. Mientras
que + funciona solo con cosas que son consideradas números, == funciona con cualquiera
cosa que pueda ser comparada. El truco está en que ambas deben ser comparables entre si.
No podemos comparar la velocidad con el tocino. Daremos un vistazo más detallado sobre los
tipos más adelante. Nota: podemos hacer 5 + 4.0 porque 5 no posee un tipo concreto y
puede actuar como un entero o como un número en coma flotante. 4.0 no puede actuar como
un entero, así que 5 es el único que se puede adaptar.
Puede que no lo sepas, pero hemos estado usando funciones durante todo este tiempo.
Por ejemplo, * es una función que toma dos números y los multiplica. Como ya has visto, lo
llamamos haciendo un sándwich sobre él. Esto lo llamamos funciones infijas. Muchas
funciones que no son usadas con números son prefijas. Vamos a ver alguna de ellas.
Las funciones normalmente son prefijas así que de ahora en adelante no vamos a decir
que una función está en forma prefija, simplemente lo asumiremos. En muchos lenguajes
imperativos las funciones son llamadas escribiendo su nombre y luego escribiendo sus
parámetros entre paréntesis, normalmente separados por comas. En Haskell, las funciones
son llamadas escribiendo su nombre, un espacio y sus parámetros, separados por espacios.
Para empezar, vamos a intentar llamar a una de las funciones más aburridas de Haskell.
ghci> succ 8
9
La función succ toma cualquier cosa que tenga definido un sucesor y devuelve ese
sucesor. Como puedes ver, simplemente hemos separado el nombre de la función y su
parámetro por un espacio. Llamar a una función con varios parámetros es igual de sencillo.
Las funciones min y max toman dos cosas que puedan ponerse en orden (¡cómo los números!)
y devuelven uno de ellos.
ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101
Sin embargo, si hubiésemos querido obtener el sucesor del producto de los números 9 y
10, no podríamos haber escrito succ 9 * 10 porque hubiésemos obtenido el sucesor de 9, el
cual hubiese sido multiplicado por 10, obteniendo 100. Tenemos que
escribir succ (9 * 10) para obtener 91.
Si una función toma dos parámetros también podemos llamarla como una función infija
rodeándola con acentos abiertos. Por ejemplo, la función div toma dos enteros y realiza una
división entera entre ellos. Haciendo div 92 10 obtendríamos 9. Pero cuando la llamamos
así, puede haber alguna confusión como que número está haciendo la división y cual está
siendo dividido. De manera que nosotros la llamamos como una función infija
haciendo 92 `div` 10, quedando de esta forma más claro.
La gente que ya conoce algún lenguaje imperativo tiende a aferrarse a la idea de que los
paréntesis indican una aplicación de funciones. Por ejemplo, en C, usas los paréntesis para
llamar a las funciones como foo(), bar(1), o baz(3, "jaja"). Como hemos dicho, los
espacios son usados para la aplicación de funciones en Haskell. Así que estas funciones en
Haskell serían foo, bar 1 y baz 3 "jaja". Si ves algo como bar (bar 3) no significa
que bar es llamado con bar y 3 como parámetros. Significa que primero llamamos a la
función bar con 3 como parámetro para obtener un número y luego volver a llamar bar otra
vez con ese número. En C, esto sería algo como bar(bar(3)).
doubleMe x = x + x
Las funciones son definidas de forma similar a como son llamadas. El nombre de la
función es seguido por los parámetros separados por espacios. Pero, cuando estamos
definiendo funciones, hay un = y luego definimos lo que hace la función. Guarda esto
como baby.hs o como tú quieras. Ahora navega hasta donde lo guardaste y
ejecuta ghci desde ahí. Una vez dentro de GHCi, escribe :l baby. Ahora que nuestro código
está cargado, podemos jugar con la función que hemos definido.
ghci> :l baby
[1 of 1] Compiling Main ( baby.hs, interpreted )
Ok, modules loaded: Main.
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6
Como + funciona con los enteros igual de bien que con los número en coma flotante (en
realidad con cualquier cosa que pueda ser considerada un número), nuestra función también
funciona con cualquier número. Vamos a hacer una función que tome dos números,
multiplique por dos cada uno de ellos y luego sume ambos.
doubleUs x y = x*2 + y*2
Como podrás deducir, puedes llamar tus propias funciones dentro de las funciones que
hagas. Teniendo esto en cuenta, podríamos redefinir doubleUs como:
doubleUs x y = doubleMe x + doubleMe y
Esto es un simple ejemplo de un patrón normal que verás por todo Haskell. Crear
funciones pequeñas que son obviamente correctas y luego combinarlas en funciones más
complejas. De esta forma también evitarás repetirte. ¿Qué pasa si algunos matemáticos
descubren que 2 es en realidad 3 y tienes que cambiar tu programa? Puedes simplemente
redefinir doubleMe para que sea x + x + x y como doubleUs llama
a doubleMe automáticamente funcionara en este extraño mundo en el que 2 es 3.
Las funciones en Haskell no tienen que estar en ningún orden en particular, así que no
importa si defines antes doubleMe y luego doubleUs o si lo haces al revés.
Ahora vamos a crear una función que multiplique un número por 2 pero solo si ese número
es menor o igual que 100, porque los número mayores 100 ya son suficientemente grandes
por si solos.
Si hubiésemos omitido los paréntesis, sólo hubiera sumado uno si x no fuera mayor que
100. Fíjate en el ' al final del nombre de la función. Ese apóstrofe no tiene ningún significado
especial en la sintaxis de Haskell. Es un carácter válido para ser usado en el nombre de una
función. Normalmente usamos ' para denotar la versión estricta de una función (una que no
es perezosa) o una pequeña versión modificada de una función o variable. Como ' es un
carácter válido para la funciones, podemos hacer cosas como esta.
conanO'Brien = "¡Soy yo, Conan O'Brien!"
Hay dos cosas que nos quedan por destacar. La primera es que el nombre de esta función
no empieza con mayúsculas. Esto se debe a que las funciones no pueden empezar con una
letra en mayúsculas. Veremos el porqué un poco más tarde. La segunda es que esta función
no toma ningún parámetro, normalmente lo llamamos una definición (o un nombre). Como no
podemos cambiar las definiciones (y las funciones) después de que las hayamos
definido, conanO'Brien y la cadena "It's a-me, Conan O'Brien!" se pueden utilizar
indistintamente.
Una introducción a las listas
Al igual que las listas de la compra de la vida real, las listas en Haskell son muy útiles. Es
la estructura de datos más utilizada y pueden ser utilizadas de diferentes formas para modelar
y resolver un montón de problemas. Las listas son MUY importantes. En esta sección daremos
un vistazo a las bases sobre las listas, cadenas de texto (las cuales son listas) y listas
intensionales.
En Haskell, las listas son una estructura de datos homogénea. Almacena varios
elementos del mismo tipo. Esto significa que podemos crear una lista de enteros o una lista de
caracteres, pero no podemos crear una lista que tenga unos cuantos enteros y otros cuantos
caracteres. Y ahora, ¡una lista!
Nota
Podemos usar la palabra reservada let para definir un nombre en GHCi.
Hacer let a = 1 dentro de GHCi es equivalente ha escribir a = 1 en un fichero y luego
cargarlo.
Como puedes ver, las listas se definen mediante corchetes y sus valores se separan por
comas. Si intentáramos crear una lista como esta [1,2,'a',3,'b','c',4], Haskell nos
avisaría que los caracteres (que por cierto son declarados como un carácter entre comillas
simples) no son números. Hablando sobre caracteres, las cadenas son simplemente listas de
caracteres. "hello" es solo una alternativa sintáctica de ['h','e','l','l','o']. Como las
cadenas son listas, podemos usar las funciones que operan con listas sobre ellas, lo cual es
realmente útil.
Una tarea común es concatenar dos listas. Cosa que conseguimos con el operador ++.
ghci> [1,2,3,4] ++ [9,10,11,12]
[1,2,3,4,9,10,11,12]
ghci> "hello" ++ " " ++ "world"
"hello world"
ghci> ['w','o'] ++ ['o','t']
"woot"
Hay que tener cuidado cuando utilizamos el operador ++ repetidas veces sobre cadenas
largas. Cuando concatenamos dos listas (incluso si añadimos una lista de un elemento a otra
lista, por ejemplo [1,2,3] ++ [4], internamente, Haskell tiene que recorrer la lista entera
desde la parte izquierda del operador ++. Esto no supone ningún problema cuando trabajamos
con listas que no son demasiado grandes. Pero concatenar algo al final de una lista que tiene
cincuenta millones de elementos llevará un rato. Sin embargo, concatenar algo al principio de
una lista utilizando el operador : (también llamado operador cons) es instantáneo.
ghci> 'U':"n gato negro"
"Un gato negro"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]
Fíjate que : toma un número y una lista de números o un carácter y una lista de
caracteres, mientras que ++ toma dos listas. Incluso si añades un elemento al final de las lista
con ++, hay que rodearlo con corchetes para que se convierte en una lista de un solo
elemento.
ghci> [1,2] ++ 3
<interactive>:1:10:
No instance for (Num [a0])
arising from the literal `3'
[...]
Si queremos obtener un elemento de la lista sabiendo su índice, utilizamos !!. Los índices
empiezan por 0.
ghci> "Steve Buscemi" !! 6
'B'
ghci> [9.4,33.2,96.2,11.2,23.25] !! 1
33.2
Pero si intentamos obtener el sexto elemento de una lista que solo tiene cuatro elementos,
obtendremos un error, así que hay que ir con cuidado.
Las listas también pueden contener listas. Estas también pueden contener a su vez listas
que contengan listas, que contengan listas...
Las listas dentro de las listas pueden tener diferentes tamaños pero no pueden tener
diferentes tipos. De la misma forma que no se puede contener caracteres y números en un
lista, tampoco se puede contener listas que contengan listas de caracteres y listas de
números.
Las listas pueden ser comparadas si los elementos que contienen pueden ser
comparados. Cuando usamos <, <=, >, y >= para comparar listas, son comparadas en orden
lexicográfico. Primero son comparadas las cabezas. Luego son comparados los segundos
elementos y así sucesivamente.
¿Qué mas podemos hacer con las listas? Aquí tienes algunas funciones básicas que
pueden operar con las listas.
head toma una lista y devuelve su cabeza. La cabeza de una lista es básicamente
el primer elemento.
ghci> head [5,4,3,2,1]
5
tail toma una lista y devuelve su cola. En otros palabras, corta la cabeza de la
lista.
ghci> tail [5,4,3,2,1]
[4,3,2,1]
init toma una lista y devuelve toda la lista excepto su último elemento.
ghci> init [5,4,3,2,1]
[5,4,3,2]
ghci> head []
*** Exception: Prelude.head: empty list
null comprueba si una lista está vacía. Si lo está, devuelve True, en caso
contrario devuelve False. Usa esta función en lugar de xs == [] (si tienes una lista
que se llame xs).
ghci> null [1,2,3]
False
ghci> null []
True
take toma un número y una lista y extrae dicho número de elementos de una
lista. Observa.
ghci> take 3 [5,4,3,2,1]
[5,4,3]
ghci> take 1 [3,9,3]
[3]
ghci> take 5 [1,2]
[1,2]
ghci> take 0 [6,6,6]
[]
Fíjate que si intentamos tomar más elementos de los que hay en una lista,
simplemente devuelve la lista. Si tomamos 0 elementos, obtenemos una lista vacía.
drop funciona de forma similar, solo que quita un número de elementos del
comienzo de la lista.
ghci> drop 3 [8,4,2,1,5,6]
[1,5,6]
ghci> drop 0 [1,2,3,4]
[1,2,3,4]
ghci> drop 100 [1,2,3,4]
[]
maximum toma una lista de cosas que se pueden poner en algún tipo de orden y
devuelve el elemento más grande.
minimum devuelve el más pequeño.
ghci> minimum [8,4,2,1,5,6]
1
ghci> maximum [1,9,2,3,4]
9
sum toma una lista de números y devuelve su suma.
product toma una lista de números y devuelve su producto.
ghci> sum [5,2,1,6,3,2,5,7]
31
ghci> product [6,2,1,2]
24
ghci> product [1,2,5,6,7,9,2,0]
0
elem toma una cosa y una lista de cosas y nos dice si dicha cosa es un elemento
de la lista. Normalmente, esta función es llamada de forma infija porque resulta más
fácil de leer.
ghci> 4 `elem` [3,4,5,6]
True
ghci> 10 `elem` [3,4,5,6]
False
Estas fueron unas cuantas funciones básicas que operan con listas. Veremos más
funciones que operan con listas más adelante.
Rangos
¿Qué pasa si queremos una lista con todos los números entre el 1 y el 20? Sí, podríamos
simplemente escribirlos todos pero obviamente esto no es una solución para los que buscan
buenos lenguajes de programación. En su lugar, usaremos rangos. Los rangos son una
manera de crear listas que contengan una secuencia aritmética de elementos enumerables.
Los números pueden ser enumerados. Uno, dos, tres, cuatro, etc. Los caracteres también
pueden ser enumerados. El alfabeto es una enumeración de caracteres desde la A hasta la Z.
Los nombres no son enumerables. ¿Qué viene después de “Juan”? Ni idea.
Para crear una lista que contenga todos los números naturales desde el 1 hasta el 20
simplemente escribimos [1..20]. Es equivalente a
escribir [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] y no hay ninguna
diferencia entre escribir uno u otro salvo que escribir manualmente una larga secuencia de
enumerables es bastante estúpido.
ghci> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
ghci> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"
ghci> [2,4..20]
[2,4,6,8,10,12,14,16,18,20]
ghci> [3,6..20]
[3,6,9,12,15,18]
Es cuestión de separar los primeros dos elementos con una coma y luego especificar el
límite superior. Aunque son inteligentes, los rangos con pasos no son tan inteligentes como
algunas personas esperan que sean. No puedes escribir [1,2,4,8,16..100] y esperar
obtener todas las potencias de 2. Primero porque solo se puede especificar un paso. Y
segundo porque las secuencias que no son aritméticas son ambiguas si solo damos unos
pocos elementos iniciales.
Para obtener una lista con todos los números desde el 20 hasta el 1 no podemos
usar [20..1], debemos utilizar [20,19..1].
¡Cuidado cuando uses números en coma flotante con los rangos! Éstos no son del todo
precisos (por definición), y su uso con los rangos puede dar algunos resultados no esperados.
También podemos utilizar los rangos para crear listas infinitas simplemente no indicando
un límite superior. Más tarde nos centraremos más en las listas infinitas. Por ahora, vamos a
examinar como obtendríamos los primeros 24 múltiplos de 13. Sí, podemos
utilizar [13,26..24*13]. Pero hay una forma mejor: take 13 [13,26..]. Como Haskell es
perezoso, no intentará evaluar la lista infinita inmediatamente porque no terminaría nunca.
Esperará a ver que es lo que quieres obtener de la lista infinita. En este caso ve que solo
queremos los primeros 24 elementos y los evalúa con mucho gusto.
Ahora, un par de funciones que generan listas infinitas:
cycle toma una lista y crea un ciclo de listas iguales infinito. Si intentáramos
mostrar el resultado nunca terminaría así que hay que cortarlo en alguna parte.
ghci> take 10 (cycle [1,2,3])
[1,2,3,1,2,3,1,2,3,1]
ghci> take 12 (cycle "LOL ")
"LOL LOL LOL "
repeat toma un elemento y produce una lista infinita que contiene ese único
elemento repetido. Es como hacer un ciclo de una lista con un solo elemento.
ghci> take 10 (repeat 5)
[5,5,5,5,5,5,5,5,5,5]
Aunque aquí sería más simple usar la función replicate, ya que sabemos el
número de elementos de antemano. replicate 3 10 devuelve [10,10,10].
Soy una lista intensional
Si alguna vez tuviste clases de matemáticas, probablemente viste algún conjunto definido
de forma intensiva, definido a partir de otros conjuntos más generales. Un conjunto definido de
forma intensiva que contenga los diez primeros números naturales pares
sería . La parte anterior al separador se llama la función de
salida, es la variable, es el conjunto de entrada y es el predicado. Esto significa
que el conjunto contiene todos los dobles de los número naturales que cumplen el predicado.
Si quisiéramos escribir esto en Haskell, podríamos usar algo como take 10 [2,4..].
Pero, ¿y si no quisiéramos los dobles de los diez primeros número naturales, sino algo más
complejo? Para ello podemos utilizar listas intensionales. Las listas intensionales son muy
similares a los conjuntos definidos de forma intensiva. En este caso, la lista intensional que
deberíamos usar sería [x*2 | x <- [1..10]]. x es extraído de [1..10] y para cada
elemento de [1..10] (que hemos ligado a x) calculamos su doble. Su resultado es:
ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
Como podemos ver, obtenemos el resultado deseado. Ahora vamos a añadir una
condición (o un predicado) a esta lista intensional. Los predicados van después de la parte
donde enlazamos las variables, separado por una coma. Digamos que solo queremos los
elementos que su doble sea mayor o igual a doce:
Bien, funciona. ¿Y si quisiéramos todos los números del 50 al 100 cuyo resto al dividir por
7 fuera 3? Fácil:
Podemos incluir varios predicados. Si quisiéramos todos los elementos del 10 al 20 que no
fueran 13, 15 ni 19, haríamos:
No solo podemos tener varios predicados en una lista intensional (un elemento debe
satisfacer todos los predicados para ser incluido en la lista), sino que también podemos
extraer los elementos de varias listas. Cuando extraemos elementos de varias listas, se
producen todas las combinaciones posibles de dichas listas y se unen según la función de
salida que suministremos. Una lista intensional que extrae elementos de dos listas cuyas
longitudes son de 4, tendrá una longitud de 16 elementos, siempre y cuando no los filtremos.
Si tenemos dos listas, [2,5,10] y [8,10,11] y queremos que el producto de todas las
combinaciones posibles entre ambas, podemos usar algo como:
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]
¿Qué tal una lista intensional que combine una lista de adjetivos con una lista de
nombres? Solo para quedarnos tranquilos...
¡Ya se! Vamos a escribir nuestra propia versión de length. La llamaremos length'.
length' xs = sum [1 | _ <- xs]
_ significa que no nos importa lo que vayamos a extraer de la lista, así que en vez de
escribir el nombre de una variable que nunca usaríamos, simplemente escribimos _. La
función reemplaza cada elemento de la lista original por 1 y luego los suma. Esto significa que
la suma resultante será el tamaño de nuestra lista.
Un recordatorio: como las cadenas son listas, podemos usar las listas intensionales para
procesar y producir cadenas. Por ejemplo, una función que toma cadenas y elimina de ellas
todo excepto las letras mayúsculas sería algo tal que así:
En este caso el predicado hace todo el trabajo. Dice que el elemento será incluido en la
lista solo si es un elemento de [A..Z]. Es posible crear listas intensionales anidadas si
estamos trabajando con listas que contienen listas. Por ejemplo, dada una lista de listas de
números, vamos eliminar los números impares sin aplanar la lista:
ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],
[1,2,4,2,1,6,3,1,3,2,3,6]]
ghci> [ [ x | x <- xs, even x ] | xs <- xxs]
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]
Podemos escribir las listas intensionales en varias líneas. Si no estamos usando GHCi es
mejor dividir las listas intensionales en varias líneas, especialmente si están anidadas.
Tuplas
De alguna forma, las tuplas son parecidas a las listas. Ambas son una forma de almacenar
varios valores en un solo valor. Sin embargo, hay unas cuantas diferencias fundamentales.
Una lista de números es una lista de números. Ese es su tipo y no importa si tiene un sólo
elemento o una cantidad infinita de ellos. Las tuplas sin embargo, son utilizadas cuando sabes
exactamente cuantos valores tienen que ser combinados y su tipo depende de cuantos
componentes tengan y del tipo de estos componentes. Las tuplas se denotan con paréntesis y
sus valores se separan con comas.
Otra diferencia clave es que no tienen que ser homogéneas. Al contrario que las listas, las
tuplas pueden contener una combinación de valores de distintos tipos.
Nos está diciendo que hemos intentado usar una dupla y una tripla en la misma lista, lo
cual no esta permitido ya que las listas son homogéneas y un dupla tiene un tipo diferente al
de una tripla (aunque contengan el mismo tipo de valores). Tampoco podemos hacer algo
como [(1,2),("uno",2)] ya que el primer elemento de la lista es una tupla de números y el
segundo es una tupla de una cadena y un número. Las tuplas pueden ser usadas para
representar una gran variedad de datos. Por ejemplo, si queremos representar el nombre y la
edad de alguien en Haskell, podemos utilizar la tripla: ("Christopher", "Walken", 55).
Como hemos visto en este ejemplo las tuplas también pueden contener listas.
Utilizamos la tuplas cuando sabemos de antemano cuantos componentes de algún dato
debemos tener. Las tuplas son mucho más rígidas que las listas ya que para cada tamaño
tienen su propio tipo, así que no podemos escribir una función general que añada un elemento
a una tupla: tenemos que escribir una función para añadir duplas, otra función para añadir
triplas, otra función para añadir cuádruplas, etc.
Mientras que existen listas unitarias, no existen tuplas unitarias. Realmente no tiene
mucho sentido si lo piensas. Una tupla unitaria sería simplemente el valor que contiene y no
nos aportaría nada útil.
Como las listas, las tuplas pueden ser comparadas si sus elementos pueden ser
comparados. Solo que no podemos comparar dos tuplas de diferentes tamaños mientras que
si podemos comparar dos listas de diferentes tamaños. Dos funciones útiles para operar con
duplas son:
Nota
Estas funciones solo operan sobre duplas. No funcionaran sobre triplas, cuádruplas,
quíntuplas, etc. Veremos más formas de extraer datos de las tuplas un poco más tarde.
Ahora una función interesante que produce listas de duplas es zip. Esta función toma dos
listas y las une en un lista uniendo sus elementos en una dupla. Es una función realmente
simple pero tiene montones de usos. Es especialmente útil cuando queremos combinar dos
listas de alguna forma o recorrer dos listas simultáneamente. Aquí tienes una demostración:
ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["uno","dos","tres","cuatro","cinco"]
[(1,"uno"),(2,"dos"),(3,"tres"),(4,"cuatro"),(5,"cinco")]
Como vemos, se emparejan los elementos produciendo una nueva lista. El primer
elemento va el primero, el segundo el segundo, etc. Ten en cuenta que como las duplas
pueden tener diferentes tipos, zip puede tomar dos listas que contengan diferentes tipos y
combinarlas. ¿Qué pasa si el tamaño de las listas no coincide?
ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["soy","una","tortuga"]
[(5,"soy"),(3,"una"),(2,"tortuga")]
Simplemente se recorta la lista más larga para que coincida con el tamaño de la más
corta. Como Haskell es perezoso, podemos usar zip usando listas finitas e infinitas:
ghci> zip [1..] ["manzana", "naranja", "cereza", "mango"]
[(1,"manzana"),(2,"naranja"),(3,"cereza"),(4,"mango")]
He aquí un problema que combina tuplas con listas intensionales: ¿Qué triángulo recto
cuyos lados miden enteros menores que 10 tienen un perímetro igual a 24? Primero, vamos a
intentar generar todos los triángulos con lados iguales o menores que 10:
Simplemente estamos extrayendo valores de estas tres listas y nuestra función de salida
las esta combinando en una tripla. Si evaluamos esto escribiendo triangles en GHCi,
obtendremos una lista con todos los posibles triángulos cuyos lados son menores o iguales
que 10. Ahora, debemos añadir una condición que nos filtre únicamente los triángulos rectos.
Vamos a modificar esta función teniendo en consideración que el lado b no es mas largo que
la hipotenusa y que el lado a no es más largo que el lado b.
ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <-
[1..b], a^2 + b^2 == c^2]
Ya casi hemos acabado. Ahora, simplemente modificaremos la función diciendo que solo
queremos aquellos que su perímetro es 24.
Cargando...
¡Y ahí está nuestra respuesta! Este método de resolución de problemas es muy común en
la programación funcional. Empiezas tomando un conjunto de soluciones y vas aplicando
transformaciones para ir obteniendo soluciones, filtrándolas una y otra vez hasta obtener las
soluciones correctas.
Un tipo es como una etiqueta que posee toda expresión. Esta etiqueta nos dice a que
categoría de cosas se ajusta la expresión. La expresión True es un booleano, "Hello" es una
cadena, etc.
Ahora vamos a usar GHCi para examinar los tipos de algunas expresiones. Lo haremos
gracias al comando :t, el cual, seguido de una expresión válida nos dice su tipo. Vamos a dar
un vistazo:
ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "HOLA!"
"HELLO!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool
Podemos ver que ejecutando el comando :t sobre una expresión se muestra esa misma
expresión seguida de :: y de su tipo. :: se puede leer como tiene el tipo. Los tipos explícitos
siempre se escriben con su primera letra en mayúsculas. 'a', como hemos visto, tiene el
tipo Char. El nombre de este tipo viene de “Character” (carácter en inglés). True tiene el
tipo Bool. Tiene sentido. Pero, ¿qué es esto? Examinando el tipo
de "HOLA!" obtenemos [Char]. Los corchetes definen una lista. Así que leemos esto como
una lista de caracteres. Al contrario que las listas, cada tamaño de tupla tiene su propio tipo.
Así que la expresión (True, 'a') tiene el tipo (Bool, Char), mientras que la
expresión ('a', 'b', 'c') tiene el tipo (Char, Char, Char). 4 == 5 siempre
devolverá False así que esta expresión tiene el tipo Bool.
Las funciones también tiene tipos. Cuando escribimos nuestras propias funciones
podemos darles un tipo explícito en su declaración. Generalmente está bien considerado
escribir los tipos explícitamente en la declaración de un función, excepto cuando éstas son
muy cortas. De aquí en adelante les daremos tipos explícitos a todas las funciones que
creemos. ¿Recuerdas la lista intensional que filtraba solo las mayúsculas de una cadena?
Aquí tienes como se vería con su declaración de tipo:
removeNonUppercase tiene el tipo [Char] -> [Char], que significa que es una función
que toma una cadena y devuelve otra cadena. El tipo [Char] es sinónimo de String así que
sería más elegante escribir el tipo como removeNonUppercase :: String -> String.
Anteriormente no le dimos un tipo a esta función ya que el compilador puede inferirlo por si
solo. Pero, ¿cómo escribimos el tipo de una función que toma varios parámetros? Aquí tienes
una función que toma tres enteros y los suma:
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
Los parámetros están separados por -> y no existe ninguna diferencia especial entre los
parámetros y el tipo que devuelve la función. El tipo que devuelve la función es el último
elemento de la declaración y los parámetros son los restantes. Más tarde veremos porque
simplemente están separados por -> en lugar de tener algún tipo de distinción más explícita
entre los tipos de parámetros y el tipo de retorno, algo como Int, Int, Int -> Int.
Si escribimos una función y no tenemos claro el tipo que debería tener, siempre podemos
escribir la función sin su tipo y ejecutar el comando :t sobre ella. Las funciones también son
expresiones así que no hay ningún problema en usar :t con ellas.
Aquí tienes una descripción de los tipos más comunes:
ghci> factorial 50
3041409320171337804361260816606476884437764156896051200000
0000000
Bool es el tipo booleano. Solo puede tener dos valores: True o False.
Char representa un carácter. Se define rodeado por comillas simples. Una lista de
caracteres es una cadena.
Las tuplas también poseen tipos pero dependen de su longitud y del tipo de sus
componentes, así que teóricamente existe una infinidad de tipos de tuplas y eso son
demasiados tipos como para cubrirlos en esta guía. La tupla vacía es también un tipo () el
cual solo puede contener un valor: ().
Variables de tipo
¿Cual crees que es el tipo de la función head? Como head toma una lista de cualquier tipo
y devuelve su primer elemento... ¿Cual podrá ser? Vamos a verlo:
ghci> :t head
head :: [a] -> a
Hmmm... ¿Qué es a? ¿Es un tipo? Si recuerdas antes dijimos que los tipos deben
comenzar con mayúsculas, así que no puede ser exactamente un tipo. Como no comienza
con una mayúscula en realidad es una variable de tipo. Esto significa que a puede ser
cualquier tipo. Es parecido a los tipos genéricos de otros lenguajes, solo que en Haskell son
mucho más potentes ya que nos permite definir fácilmente funciones muy generales siempre
que no hagamos ningún uso especifico del tipo en cuestión. Las funciones que tienen
variables de tipos son llamadas funciones polimórficas. La declaración de
tipo head representa una función que toma una lista de cualquier tipo y devuelve un elemento
de ese mismo tipo.
Aunque las variables de tipo pueden tener nombres más largos de un solo carácter,
normalmente les damos nombres como a, b, c, d, etc.
¿Recuerdas fst? Devuelve el primer componente de una dupla. Vamos a examinar su
tipo.
ghci> :t fst
fst :: (a, b) -> a
Como vemos, fst toma una dupla que contiene dos tipos y devuelve un elemento del
mismo tipo que el primer componente de la dupla. Ese es el porqué de que podamos
usar fst con duplas que contengan cualquier combinación de tipos. Ten en cuenta que solo
porque a y b son diferentes variables de tipo no tienen porque ser diferentes tipos.
Simplemente representa que el primer componente y el valor que devuelve la función son del
mismo tipo.
Clases de tipos paso a paso (1ª parte)
Las clases de tipos son una especie de interfaz que define algún tipo de comportamiento.
Si un tipo es miembro de una clase de tipos, significa que ese tipo soporta e implementa el
comportamiento que define la clase de tipos. La gente que viene de lenguajes orientados a
objetos es propensa a confundir las clases de tipos porque piensan que son como las clases
en los lenguajes orientados a objetos. Bien, pues no lo son. Una aproximación más adecuada
sería pensar que son como las interfaces de Java, o los protocolos de Objective-C, pero
mejor.
Nota
El operador de igualdad == es una función. También lo son +, -, *, / y casi todos los
operadores. Si el nombre de una función está compuesta solo por caracteres especiales (no
alfanuméricos), es considerada una función infija por defecto. Si queremos examinar su tipo,
pasarla a otra función o llamarla en forma prefija debemos rodearla con paréntesis. Por
ejemplo: (+) 1 4 equivale a 1 + 4.
Interesante. Aquí vemos algo nuevo, el símbolo =>. Cualquier cosa antes del
símbolo => es una restricción de clase. Podemos leer la declaración de tipo anterior como: la
función de igualdad toma dos parámetros que son del mismo tipo y devuelve un Bool. El tipo
de estos dos parámetros debe ser miembro de la clase Eq (esto es la restricción de clase).
La clase de tipos Eq proporciona una interfaz para las comparaciones de igualdad.
Cualquier tipo que tenga sentido comparar dos valores de ese tipo por igualdad debe ser
miembro de la clase Eq. Todos los tipos estándar de Haskell excepto el tipo IO (un tipo para
manejar la entrada/salida) y las funciones forman parte de la clase Eq.
La función elem tiene el tipo (Eq a) => a -> [a] -> Bool porque usa == sobre los
elementos de la lista para saber si existe el elemento indicado dentro de la lista.
Algunas clases de tipos básicas son:
Eq es utilizada por los tipos que soportan comparaciones por igualdad. Los
miembros de esta clase implementan las funciones == o /= en algún lugar de su
definición. Todos los tipos que mencionamos anteriormente forman parte de la
clase Eq exceptuando las funciones, así que podemos realizar comparaciones de
igualdad sobre ellos.
ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho Ho" == "Ho Ho"
True
ghci> 3.432 == 3.432
True
Todos los tipos que hemos llegado a ver excepto las funciones son parte de la
clase Ord. Ord cubre todas las funciones de comparación como >, <, >= y <=. La
función compare toma dos miembros de la clase Ord del mismo tipo y devuelve su
orden. El orden está representado por el tipo Ordering que puede tener tres valores
distintos: GT, EQ y LT los cuales representan mayor que, igual que y menor que,
respectivamente.
Para ser miembro de Ord, primero un tipo debe ser socio del prestigioso y exclusivo
club Eq.
ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT
Los miembros de Show pueden ser representados por cadenas. Todos los tipos
que hemos visto excepto las funciones forman parte de Show. la función más utilizada
que trabaja con esta clase de tipos es la función show. Toma un valor de un tipo que
pertenezca a la clase Show y lo representa como una cadena de texto.
ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"
Read es como la clase de tipos opuesta a Show. La función read toma una
cadena y devuelve un valor del tipo que es miembro de Read.
ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]
Hasta aquí todo bien. Una vez más, todo los tipos que hemos visto excepto las
funciones forman parte de esta clase de tipos. Pero, ¿Qué pasa si simplemente
usamos read "4"?
ghci> read "4"
<interactive>:1:0:
Ambiguous type variable `a' in the constraint:
`Read a' arising from a use of `read' at
<interactive>:1:0-7
Probable fix: add a type signature that fixes these type
variable(s)
Lo que GHCi no está intentado decir es que no sabe que queremos que devuelva. Ten
en cuenta que cuando usamos anteriormente read lo hicimos haciendo algo luego con
el resultado. De esta forma, GHCi podía inferir el tipo del resultado de la función read.
Si usamos el resultado de aplicar la función como un booleano, Haskell sabe que tiene
que devolver un booleano. Pero ahora, lo único que sabe es que queremos un tipo de
la clase Read, pero no cual. Vamos a echar un vistazo a la declaración de tipo de la
función read.
ghci> :t read
read :: (Read a) => String -> a
¿Ves? Devuelve un tipo que es miembro de la clase Read, pero si luego no lo usamos
en ningún otro lugar, no hay forma de saber que tipo es. Por este motivo utilizamos
las anotaciones de tipo explícitas. Las anotación de tipo son una forma de decir
explícitamente el tipo que debe tener una expresión. Lo hacemos añadiendo :: al final
de la expresión y luego especificando el tipo. Observa:
ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
La mayoría de expresiones son del tipo que el compilador puede inferir por si solo.
Pero a veces, el compilador desconoce el tipo de valor que debe devolver una
expresión como read "5", que podría ser Int, Double, etc. Para saberlo, Haskell
debe en realidad evaluar read "5". Pero como Haskell es un lenguaje con tipos
estáticos, debe conocer todos los tipos antes de que el código sea compilado (o en
GHCi, evaluado). Así que con esto le estamos diciendo a Haskell: “Ey, esta expresión
debe ser de este tipo en caso de que no sepas cual es”.
Los miembros de la clase Enum son tipos secuencialmente ordenados, es decir,
pueden ser enumerados. La principal ventaja de la clase de tipos Enum es que
podemos usar los miembros en las listas aritméticas. También tienen definidos los
sucesores y predecesores, por lo que podemos usar las funciones succ y pred. Los
tipos de esta clase son: (), Bool, Char, Ordering, Int, Integer, Float y Double.
ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'
Parece que todos los números son también constantes polimórficas. Pueden actuar
como si fueran cualquier tipo de la clase Num.
ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0
Estos son los tipo estándar de la clase Num. Si examinamos el tipo de * veremos que
puede aceptar cualquier tipo de número.
ghci> :t (*)
(*) :: (Num a) => a -> a -> a
Toma dos números del mismo tipo y devuelve un número del mismo tipo. Esa es la
razón por la que (5 :: Int) * (6 :: Integer) lanzará un error mientras
que 5 * (6 :: Integer) funcionará correctamente y producirá un Interger, ya
que 5 puede actuar como un Integer o un Int.
Para unirse a Num, un tipo debe ser amigo de Show y Eq.
Integral es también un clase de tipos numérica. Num incluye todos los números,
incluyendo números reales y enteros. Integral únicamente incluye números
enteros. Int e Integer son miembros de esta clase.
Floating incluye únicamente números en coma flotante, es
decir Float y Double.
Una función muy útil para trabajar con números es fromIntegral. Tiene el
tipo fromIntegral :: (Num b, Integral a) => a -> b. A partir de esta declaración
podemos decir que toma un número entero y lo convierte en un número más general. Esto es
útil cuando estas trabajando con números reales y enteros al mismo tiempo. Por ejemplo, la
función length tiene el tipo length :: [a] -> Int en vez de tener un tipo más general
como (Num b) => length :: [a] -> b. Creo que es por razones históricas o algo
parecido, en mi opinión, es absurdo. De cualquier modo, si queremos obtener el tamaño de
una lista y sumarle 3.2, obtendremos un error al intentar sumar un entero con uno en coma
flotante. Para solucionar esto, hacemos fromIntegral (length [1,2,3,4]) + 3.2.
Cargando...
Fíjate que en la declaración de tipo de fromIntegral hay varias restricciones de clase. Es
completamente válido como puedes ver, las restricciones de clase deben ir separadas por
comas y entre paréntesis.
El ajuste de patrones también pueden ser usado con tuplas. ¿Cómo crearíamos una
función que tomara dos vectores 2D (representados con duplas) y que devolviera la suma de
ambos? Para sumar dos vectores sumamos primero sus componentes x y sus
componentes y de forma separada. Así es como lo haríamos si no existiese el ajuste de
patrones:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
Bien, funciona, pero hay mejores formas de hacerlo. Vamos a modificar la función para
que utilice un ajuste de patrones.
Las listas también pueden ser usadas en un ajuste de patrones. Puedes comparar contra
la lista vacía [] o contra cualquier patrón que involucre a : y la lista vacía. Como [1,2,3],
que solo es otra forma de expresar 1:2:3:[] (podemos utilizar ambas alternativas). Un patrón
como x:xs ligará la cabeza de la lista con x y el resto con xs, incluso cuando la lista tenga
solo un elemento, en cuyo caso xs acabará siendo la lista vacía.
Nota
El patrón x:xs es muy utilizado, especialmente con las funciones recursivas. Los patrones que
contengan un : solo aceptarán listas con algún elemento.
Si quisiéramos ligar, digamos, los tres primeros elementos de una lista a variables y el
resto a otra variable podemos usar algo como x:y:z:zs. Sin embargo esto solo aceptará
listas que tengan al menos 3 elementos.
Ahora que ya sabemos usar patrones con las listas vamos a implementar nuestra propia
función head.
head' :: [a] -> a
head' [] = error "¡Hey, no puedes utilizar head con una lista vacía!"
head' (x:_) = x
Comprobamos que funciona:
Fíjate que no hay un = después del nombre de la función y sus parámetros, antes de la
primera guarda. Muchos novatos obtienen un error sintáctico por poner un = ahí, y tú también
lo harás.
Otro ejemplo muy simple: vamos a implementar nuestra función max. Si recuerdas, puede
tomar dos cosas que puedan ser comparadas y devuelve la mayor.
max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b
Las guardas también pueden ser escritas en una sola línea, aunque advierto que es mejor
no hacerlo ya que son mucho menos legibles, incluso con funciones cortas. Pero para
demostrarlo podemos definir max' como:
max' :: (Ord a) => a -> a -> a
max' a b | a > b = a | otherwise = b
¡Arg! No se lee fácilmente. Sigamos adelante. Vamos a implementar nuestro
propio compare usando guardas.
myCompare :: (Ord a) => a -> a -> Ordering
a `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
ghci> 3 `myCompare` 2
GT
Nota
No solo podemos llamar a funciones de forma infija usando las comillas, sino que también
podemos definirlas de esta forma. A veces es más fácil leerlo así.
¿Dónde?
En la sección anterior definimos la función que calculaba el IMC así:
ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar =
"there!" in foo ++ bar)
(6000000,"Hey there!")
No tenemos porque poner el último punto y coma pero podemos hacerlo si queremos.
Como ya hemos dicho, podemos utilizar ajustes de patrones con las expresiones let. Son
muy útiles para desmantelar tuplas en sus componentes y ligarlos a varias variables.
ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100
600
También podemos usar las secciones let dentro de las listas intensionales. Vamos a
reescribir nuestro ejemplo anterior que calculaba una lista de duplas de alturas y pesos para
que use un let dentro de una lista intensional en lugar de definir una función auxiliar con
un where.
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
Incluimos un let dentro de la lista intensional como si fuera un predicado, solo que no
filtra los elementos, únicamente liga variables. Las variables definidas en una
expresión let dentro de una lista intensional son visibles desde la función de salida (la parte
anterior a |) y todos los predicados y secciones que vienen después de su definición.
Podríamos hacer que nuestra función devolviera el IMC solo para la gente obesa así:
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
No podemos usar el nombre bmi dentro de la parte (w, h) <- xs ya que está definida
antes que la expresión let.
Omitimos la parte in de las secciones let dentro de las lista intensionales porque la
visibilidad de los nombres está predefinida en estos casos. Sin embargo, podemos usar una
sección let in en un predicado y las variables definidas solo serán visibles en este
predicado. La parte in también puede ser omitida cuando definimos funciones y constantes
dentro del intérprete GHCi. Si lo hacemos, las variables serán visibles durante toda la sesión.
ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
<interactive>:1:0: Not in scope: `boot'
Si las expresiones let son tan interesantes, ¿por qué no usarlas siempre en lugar de las
secciones where? Bueno, como las expresiones let son expresiones y son bastante locales
en su ámbito, no pueden ser usadas entre guardas. Hay gente que prefiere las
secciones where porque las variables vienen después de la función que los utiliza. De esta
forma, el cuerpo de la función esta más cerca de su nombre y declaración de tipo y algunos
piensan que es más legible.
Expresiones case
Muchos lenguajes imperativos (como C, C++, Java, etc.) tienen construcciones
sintácticas case y si alguna vez has programado en ellos, probablemente sepas acerca de
que va esto. Se trata de tomar una variable y luego ejecutar bloques de código para ciertos
valores específicos de esa variable y luego incluir quizá algún bloque que siempre se ejecute
en caso de que la variable tenga algún valor que no se ajuste con ninguno de los anteriores.
Haskell toma este concepto y lo lleva un paso más allá. Como su nombre indica las
expresiones case son, bueno, expresiones, como las expresiones if else o las
expresiones let. No solo podemos evaluar expresiones basándonos en los posibles valores
de un variable sino que podemos realizar un ajuste de patrones. Mmmm... tomar un valor,
realizar un ajuste de patrones sobre él, evaluar trozos de código basados en su valor, ¿dónde
hemos oído esto antes? Oh sí, en los ajuste de patrones de los parámetros de una función.
Bueno, en realidad es una alternativa sintáctica para las expresiones case. Estos dos trozos
de código hacen lo mismo y son intercambiables:
head' :: [a] -> a
head' [] = error "¡head no funciona con listas vacías!"
head' (x:_) = x
head' :: [a] -> a
head' xs = case xs of [] -> error "¡head no funciona con listas
vacías!"
(x:_) -> x
Como puedes ver la sintaxis para las expresiones case es muy simple.
case expresion of patron -> resultado
patron -> resultado
patron -> resultado
...
La expresión es ajustada contra los patrones. La acción de ajuste de patrones se
comporta como se espera: el primer patrón que se ajuste es el que se utiliza. Si no se puede
ajustar a ningún patrón de la expresión case se lanzará un error de ejecución.
Mientras que el ajuste de patrones de los parámetros de una función puede ser realizado
únicamente al definir una función, las expresiones case pueden ser utilizadas casi en
cualquier lugar. Por ejemplo:
describeList :: [a] -> String
describeList xs = "La lista es" ++ case xs of [] -> "una lista
vacía."
[x] -> "una lista
unitaria."
xs -> "una lista
larga."
Son útiles para realizar un ajuste de patrones en medio de una expresión. Como el ajuste
de patrones que se realiza en la definición de una función es una alternativa sintáctica a las
expresiones case, también podríamos utilizar algo como esto:
describeList :: [a] -> String
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."