Tipos de Datos en Erlang (I): Arrays

Todos los que llevamos un tiempo con Erlang conocemos los tipos de datos que provee (listas y listas binarias, tuplas, registros, enteros, decimales y átomos). En herencia de los tipos de datos imperativos y como modelos de datos que tienen sus propios algoritmos de acceso y son muy empleados en programación imperativa y sin embargo quedan fuera están los diccionarios, los arrays, los conjuntos, los árboles… estos tipos de datos no están presentes en la base del lenguaje, pero sí que están presentes en la librería estándar de Erlang.

En la siguiente serie de artículos daré un repaso a las construcciones que podemos encontrar en stdlib como son: array, dict, orddict, sets, gb_sets y gb_trees. Comenzamos en esta primera entrega con los arrays.

Iniciando Arrays

Los arrays son como las listas pero accesibles a través de sus índices numéricos. Como en la mayoría de lenguajes funcionales, sus elementos se numeran desde cero hasta el tamaño total (menos uno). Hay dos tipos de arrays que se pueden definir en Erlang, los arrays de tamaño variable y los de tamaño fijo.

Se definen de la siguente forma:

%% array de tamaño variable
VarArray = array:new().
 
%% array de tamaño fijo: 20 elementos
FixArray = array:new(20).

El array fijo se encarga de que el tamaño del array definido y pasado como parámetro no crezca. Se puede hacer un array variable fijo a través del uso de la función fix/1 y volver a convertirse en tamaño variable con el uso de la función relax/1.

La inicialización también se puede realizar pasando opciones, como el valor por defecto (en caso de que el índice no se haya configurado a ningún valor), el tamaño del array o si es de tamaño fijo:

array:new([{size,10},{fixed,false},{default,-1}]).

Manejando datos

La ventaja de los arrays radica en el acceso directo a los datos, tanto para almacenar como para rescatar. No obstante las variables siguen siendo inmutables, con lo que cada cambio en el array debe de ser almacenado en otra variable o en la pila de una función a modo de estado… o en el diccionario de un proceso.

Podemos tomar la información de un array y almacenarla de la siguiente forma:

%% creamos el array
Array0 = array:new(),
 
%% almacena elementos
Array1 = array:set(5, "dato", Array0).
 
%% obtiene el elemento:
array:get(5, Array1).
 
%% eliminar el valor:
Array2 = array:reset(5, Array1).

El módulo dispone además de un par de funciones que para comparar los tipos de datos (aunque no pueden ser empleadas como guardas al no ser funciones de tipo BIF o NIF) ya sea si es un is_array/1 o incluso si es un array fijo con is_fix/1.

Recorriendo el array

A través de funciones como foldl/3, foldr/3 y map/2 podemos realizar recorridos del array según lo que necesitemos en cada momento. Si requerimos de realizar una acción de orden creciente en el array con un acumulador usaremos foldl/3 y si es en caso decreciente emplearemos foldr/3. Si queremos el resultado de la ejecución independiente por cada elemento del array emplearemos map/2.

Un ejemplo con acumulador:

array:foldl(fun(I, X, Acc) ->
    io:format("~p=~p~n", [I, X]),
    I + Acc
end, , array:new(5)).
% 0=undefined
% 1=undefined
% 2=undefined
% 3=undefined
% 4=undefined
% 10

Con map/2 el código sería:

array:map(fun(I, X) ->
    io:format("~p=~p~n", [I,X]),
    I
end, array:new(5)).
% 0=undefined
% 1=undefined
% 2=undefined
% 3=undefined
% 4=undefined
% {array,5,0,undefined,
%        {0,1,2,3,4,undefined,undefined,undefined,undefined,
%        undefined}}

En este caso obtenemos la misma salida por pantalla, pero el retorno es un array con cada retorno independiente de la clausura en su índice correspondiente.

Sólo los elementos

En los casos vistos en la sección anterior el array estaba vacío. Si quisiéramos recorrer únicamente los elementos insertados en el array podemos usar sparse_foldl/3, sparse_foldr/3 o sparse_map/3. Si ejecutásemos los mismos ejemplos vistos antes, veríamos que el resultado sería 0 para el primer ejemplo y un array vacío en el segundo.

Entre estas funciones también se encuentran sparse_size/1 que retorna cuántos elementos hay hasta llegar al primer elemento configurado en el array, por ejemplo:

Array = array:set(3, "dato", array:new(10)),
array:sparse_size(Array).
% 4

Conversiones

Por último, comentar que también disponemos de un par de funciones para convertir el array en otro tipo de datos. Por ejemplo:

Array = array:set(3, "dato", array:new(10)),
 
%% Array a lista:
array:to_list(Array).
% [undefined,undefined,undefined,"dato",undefined,undefined,
%  undefined,undefined,undefined,undefined]
 
%% Array a diccionario ordenado (orddict):
array:to_orddict(Array).
% [{0,undefined},
%  {1,undefined},
%  {2,undefined},
%  {3,"dato"},
%  {4,undefined},
%  {5,undefined},
%  {6,undefined},
%  {7,undefined},
%  {8,undefined},
%  {9,undefined}]

Este último recuerda a una lista de propiedades (o proplists). Podemos realizar el paso inverso a través de las funciones to_list/1 y to_orddict/1 y disponemos además de las mismas funciones con el prefijo sparse_ para convertir únicamente los elementos dentro del array con sus índices correspondientes.

Conclusiones

En programación funcional el uso de arrays no impera como en el caso de los lenguajes imperativos. No obstante existen algoritmos que se simplifican, incluso en el ámbito de los lenguajes funcionales al poder disponer de un sistema que permita acceder directamente a los datos a través de un índice numérico. En este sentido podríamos incluso emplear las listas de siempre y transformar a array solo cuando requiramos de esta característica.