Translate

jueves, 22 de febrero de 2024

Listas en Erlang


Las listas se utilizan para resolver todo tipo de problemas y son sin duda la estructura de datos más utilizada en Erlang. ¡Las listas pueden contener cualquier cosa! Números, átomos, tuplas, otras listas. La notación básica de una lista es [Elemento1, Elemento2,..., ElementoN] y puedes mezclar más de un tipo de datos en ella:


> [1, 2, 3, {numbers,[4,5,6]}, 5.34, atom].

[1,2,3,{numbers,[4,5,6]},5.34,atom]


Bastante simple, ¿verdad?

2> [97, 98, 99].
"abc"

Los strings son listas y la notación es absolutamente la misma ¿Por qué a la gente no le gusta? Debido a esto:

3> [97,98,99,4,5,6].
[97,98,99,4,5,6]
4> [233].
"é"

Erlang imprimirá listas de números como números solo cuando al menos uno de ellos no pueda representar como una letra. 

Es por eso que quizás hayas escuchado que se dice que Erlang es malo en la manipulación de cadenas: no hay un tipo de cadena incorporado como en la mayoría de los otros lenguajes. Esto se debe a los orígenes de Erlang como lenguaje creado y utilizado por empresas de telecomunicaciones. Nunca (o rara vez) usaron cadenas y, como tal, nunca tuvieron ganas de agregarlas oficialmente. Sin embargo, la mayor parte de la falta de sentido de Erlang en las manipulaciones de cadenas se está solucionando con el tiempo: la máquina virtual ahora admite de forma nativa cadenas Unicode y, en general, se vuelve más rápida en las manipulaciones de cadenas todo el tiempo.

También hay una manera de almacenar cadenas como una estructura de datos binarios, lo que las hace realmente livianas y más rápidas para trabajar. Con todo, todavía faltan algunas funciones en la biblioteca estándar y, si bien el procesamiento de cadenas es definitivamente factible en Erlang, existen lenguajes algo mejores para tareas que necesitan mucho, como Perl o Python.

Para unir listas, usamos el operador ++. Lo opuesto a ++ es -- y eliminará elementos de una lista:

5> [1,2,3] ++ [4,5].
[1,2,3,4,5]
6> [1,2,3,4,5] -- [1,2,3].
[4,5]
7> [2,4,2] -- [2,4].
[2]
8> [2,4,2] -- [2,4,2].
[]

Tanto ++ como -- son asociativos por la derecha. Esto significa que los elementos de muchas operaciones -- o ++ se realizarán de derecha a izquierda, como en los siguientes ejemplos:

9>[1,2,3]-[1,2]-[3].
[3]
10>[1,2,3]-[1,2]-[2].
[2,3]

El primer elemento de una lista se denomina Encabezado y el resto de la lista se denomina Cola. Usaremos dos funciones integradas (BIF) para obtenerlas.

11> hd([1,2,3,4]).
1
12>tl([1,2,3,4]).
[2,3,4]

Las funciones integradas (BIF) suelen ser funciones que no se pueden implementar en Erlang puro y, como tales, se definen en C, o en cualquier lenguaje en el que se implemente Erlang (era Prolog en los años 80). Todavía hay algunos BIF que se pueden realizar en Erlang pero que aún se implementaron en C para proporcionar más velocidad a las operaciones comunes. Un ejemplo de esto es la función length(List), que devolverá la longitud (lo has adivinado) de la lista pasada como argumento.

Como se usa con tanta frecuencia, existe una forma más sencilla de separar el principio del final de una lista con la ayuda de la coincidencia de patrones: [Cabeza|Cola]. Así es como agregarías un nuevo encabezado a una lista:

13> Lista = [2,3,4].
[2,3,4]
14>NuevaLista = [1|Lista].
[1,2,3,4]

Al procesar listas, como normalmente se comienza con la cabeza, podemos desear una forma rápida de almacenar también la cola para operarla más tarde. Si recuerdas la forma en que funcionan las tuplas y cómo usamos la coincidencia de patrones para descomprimir los valores de un punto ({X,Y}), sabrás que podemos cortar el primer elemento (el encabezado) de una lista de manera similar. .

15> [Cabeza|Cola] = NuevaLista.
[1,2,3,4]
16> Cabeza.
1
17> Cola.
[2,3,4]
18> [NuevaCabeza|NuevaCola] = Cola.
[2,3,4]
19> Nuevo jefe.
2

El | que utilizamos se llama operador de contras (constructor). De hecho, cualquier lista se puede construir sólo con el operador:

20> [1 | []].
[1]
21> [2 | [1 | []]].
[2,1]
22> [3 | [2 | [1 | []] ] ].
[3,2,1]

Es decir cualquier lista se puede construir con la siguiente fórmula: [Término1| [Término2 | [... | [TérminoN]]]].... Las listas pueden así definirse recursivamente como un principio que precede a un final, que a su vez es un principio seguido de más principios. En este sentido, podríamos imaginar una lista como una lombriz de tierra: puedes cortarla por la mitad y luego tendrás dos lombrices.

Las formas en que se pueden construir las listas de Erlang a veces resultan confusas para las personas que no están acostumbradas a constructores similares. Para ayudarle a familiarizarse con el concepto, lea todos estos ejemplos (pista: todos son equivalentes):

[a B C D]
[a, b, c, d | []]
[a, b | [cd]]
[a, b | [c | [d]]]
[un | [b | [c | [d]]]]
[un | [b | [c | [d | [] ]]]]

Una vez entendido esto, debería poder lidiar con listas por comprensión.

Usando el formulario [1 | 2] ofrece lo que llamamos una "lista inadecuada". Las listas inadecuadas funcionarán cuando coincida el patrón en la forma [Cabeza|Cola], pero no podrán usarse con funciones estándar de Erlang (longitud uniforme()). Esto se debe a que Erlang espera listas adecuadas. Las listas adecuadas terminan con una lista vacía como última celda. Al declarar un elemento como [2], la lista se forma automáticamente de manera adecuada. ¡Como tal, [1|[2]] funcionaría! Las listas inadecuadas, aunque sintácticamente válidas, tienen un uso muy limitado fuera de las estructuras de datos definidas por el usuario.