Haskell Listas - Parte 1
Haskell Listas - Parte 1
Repartido: Listas
Dado cualquier tipo t, queremos definir el tipo [ t ], cuyos objetos son las listas (secuencias)
de objetos de tipo t.
La tercera línea muestra que es posible formar listas de funciones, y la cuarta es una lista
de listas.
Una lista puede contener cualquier cantidad de elementos (también llamados sus
miembros).
Esto incluye el caso de la lista vacía la cual, como es de esperar, se escribe [ ].
En Haskell todos los miembros de una lista deben ser del mismo tipo.
La notación con corchetes: [a1, a2, ..., an] se utiliza en Haskell para formar listas concretas.
Pero, en los hechos, esta notación es una abreviatura de otra notacióon más básica, que
permite formar las listas de cualquier tipo de manera sistemática y recursiva:
t tipo__
[ t ] tipo
__________ a :: t l :: [ t ]
[ ] :: [ t ] a : l :: [ t ]
Así pues, si traducimos la lista [True, False, False] a la notación “cons-nil”, resulta que se
obtiene por agregar el elemento True a la lista [False, False], lo cual nos da como notación
correcta: True : [False, False].
En este caso, como en el de toda lista no vacıa, el primer elemento se denomina la
cabeza (en inglés head) de la lista, mientras que la lista que sigue a continuación se
denomina su resto o cola (en inglés: tail).
En Haskell los paréntesis que aparecen en esta última expresión no son necesarios ya que
el operador : asocia a derecha, por lo que podemos escribir directamente:
True : False : False : [ ].
En definitiva, una lista [a1, a2, ..., an] se corresponde con la expresión a
1 : a2 : ... an : [ ].
Esta notación, junto con la convención de asociación a derecha recién mencionada, nos da
una descripción del proceso de creación o construcción (recursiva) de la lista:
- Luego vamos agregando cada vez un nuevo elemento a la lista que ya hemos formado
utilizando el operador :.
Las listas crecen entonces hacia la izquierda o, como también puede verse, agregando
elementos adelante.
Definimos en Haskell el tipo de las listas del siguiente modo:
- El nombre de tipo introducido es [ t ], donde t es un parámetro de tipo.
Es decir, estamos diciendo: para todo tipo t tenemos el tipo inductivo (data) [ t ], leído
“listas de elementos de tipo t”. O sea que, de hecho, estamos introduciendo una cantidad
infinita de tipos mediante una única declaración (observar que el parámetro t se escribe con
minúscula, indicando que no es un tipo específico con Bool o N).
- Luego viene otro constructor, que es (:). A diferencia del generador S de los naturales,
éste no forma un valor de por sí, sino que requiere dos parámetros:
- La cabeza de la nueva lista, o sea el elemento a agregar (obviamente de tipo t)
- La lista a alargar con ese nuevo elemento (obviamente de tipo [ t ]).
Asociado al tipo definido tenemos la expresión case correspondiente, cuya regla será:
e :: [ t ] e1 :: s e2 :: s [x::t, xs::[t]]
case e of { [ ] → e1 ; x:xs → e2 } :: s
Observemos que en la segunda rama del case estamos considerando que el discriminante
(e) resulte ser una lista construída agregando un elemento x a una lista xs (o sea, x
:xs).
Indicamos que tanto x como xs pueden aparecer en e2 en la tercer premisa de la regla,
donde escribimos el juicio e2 :: s [x::t, xs::[t]].
Por supuesto, los nombres utilizados para estos parámetros son arbitrarios y bien podrían
haber sido cabeza y cola, h y t, o cualesquiera otros. Pero la elección x y xs se ha impuesto
en la comunidad de programadores Haskell al punto de constituir prácticamente un
estándar. Viene del hecho de que hace notar muy gráficamente que x es un elemento
arbitrario y que el resto son “otros xs”, o sea, una lista del mismo tipo de cosas.
case [ ] of { [ ] → e1 ; x:xs → e2 } = e1
case a:l of { [ ] → e1 ; x:xs → e2 } = e2 [x:=a , xs:= l]
Observar que el resultado de la expresión case cuando el discriminante es una lista (no
vacía) de la forma a:l es e2, donde debemos instanciar x con a y xs con l, lo cual se define
utilizando la sustitución [x:=a , xs:= l].
2. Recursión en Listas
Comenzamos con un ejemplo sencillo, la función null que verifica si una lista está vacía:
null :: [ a ] → Bool
(en general en Haskell los parámetros de tipo se escriben con las primeras letras del
alfabeto: a, b, c...)
Esta es una función sencilla, que devuelve True para el caso [ ] y False para el caso x:xs:
La siguiente función sum :: [N] → N, suma todos los elementos de una lista de naturales.
Donde:
- En ?2 estamos considerando el caso de una lista formada por el constructor (:), es decir,
se trata de una lista no vacía, cuyo primer elemento es x y su resto (o cola) es la lista xs.
Ahora la cuestión es resolver la incógnita ?2 y así terminar de definir la función.
En este punto es que la recursión viene en nuestra ayuda:
¿Cuál es el resultado de sumar todos los elementos de la lista x:xs?
Bueno, basta con sumar x a la suma de los elementos de la lista xs, y esta última suma la
podemos obtener por medio de la llamada recursiva: sum xs.
Entonces ?2 = x + sum xs.
sum :: [N] → N
sum= λl→ case l of { [ ] → 0 ; x:xs → x + sum xs }
Al igual que con los Naturales, podemos plantear un esquema de recursión estructural en
listas que nos da un método de programación de funciones:
donde:
- r es el paso recursivo r, es decir el resultado de f en x:xs, para lo cual se puede utilizar el
resultado de f xs.
3. Ejemplos
Terminamos este repartido definiendo algunas funciones muy útiles del Preludio de Haskell:
Claramente ?1 = 0.
y la definición será
map (>0) [0, S 0, S(S 0), S(S(S 0))] = [False, True, True,True]
La función filter recibe un predicado y una lista, y devuelve otra lista con aquellos
elementos para los cuales el predicado se cumple.
La función (++) concatena dos listas del mismo tipo, poniendo la segunda a continuación de
la primera.
Finalmente, la función reverse da vuelta una lista, invirtiendo el orden de sus elementos:
Notar que en caso x:xs, debemos poner a x al final de la lista xs invertida. Para hacer eso,
no podemos utilizar el constructor (:), ya que éste recibe primero un elemento y después
una lista (y no al revés). Por lo tanto, debemos utilizar la función ++ que permite concatenar
dos listas, para lo cual, creamos la lista [x] que contiene sólo al elemento x, y lo colocamos
detrás de reverse xs.