Los registros como tipos de datos compuestos

Déjemos de lado por ahora la programación de alto orden y centremos nuestra atención a un concepto sumamente importante en la programación simbólica: la noción de un registro. Una vez que dominemos esta idea podremos posteriormente revisar la abstracción de datos con mucha mayor facilidad. En particular, nos brindará gran asistencia haciendo la abstracción de datos algo mucho, mucho más legible. Bien, hablemos entonces de los registros. Un registro es un tipo de datos compuesto. En cierto sentido, son un tipo de datos compuesto de propósito general, ya que en base a ellos podemos construir muchos otros tipos de datos compuestos. Por supuesto, que sea un tipo de datos compuesto no nos dice mucho, más allá de que están formados de tipos de datos más simples. O usando otro término, que son estructuras de datos. Para comprender bien la idea de registro, primero es necesario comprender bien un concepto más simple. El concepto de átomo. Por átomo, en este contexto por supuesto, se denomina a un valor o constante simbólica. Es decir a una «representación textual» que significa algo en sí misma. Estos átomos o también conocidos como símbolos no se deben confundir con los identificadores. Los identificadores son representaciones que enlazan a una variable asignada a un valor, en cambio, los símbolos representan algo en sí mismos (no tienen una variable enlazada a ellos). Dependiendo del lenguaje, considerando aquellos que los incluyen, la sintaxis para representarlos puede cambiar bastante. Para el caso de nuestro pseudocódigo, representaremos los símbolos de aquí en adelante mediante cualquier secuencia arbitraria de caracteres en minúsculas precedida de un arroba (@) (@simbolo, @otrosimbolo). Adicionalmente, como iremos viendo, con los símbolos podemos realizar computaciones como con cualquier otro valor. Podemos compararlos, incluirlos en estructuras de datos, imprimirlos, etc. Bien, ahora ya entendido el concepto de átomo o símbolo podemos pasar a la idea principal en esta lección, los registros.

Registros

Un registro es una estructura que agrupa a un conjunto de valores dentro de un único valor compuesto. A diferencia de las listas, que también se pueden ver como una agrupación de valores, en los registros se puede acceder directamente a los valores que almacena. Es decir, no hay necesidad de realizar iteraciones para acceder a cualquiera de los valores almacenados, pues cada valor, también llamado campo en este contexto, está vinculado a un nombre de campo mediante el cual se puede realizar el acceso directo. En específico, la estructura de un registro está compuesta de dos partes. Por un lado cuentan con una etiqueta, y por el otro con un conjunto de pares de nombres de campo y sus respectivos campos. Tanto la etiqueta como los nombres de campo deben ser átomos(o símbolos). Adicionalmente, los nombres de campo también pueden ser enteros. En el caso de los campos, como se mencionó más arriba, estos no son más que un nombre para referirse a los valores de un registro, y por lo tanto, pueden ser valores de cualquier tipo (incluyendo funciones y otros símbolos). Tomando todo lo dicho, podríamos por lo tanto, definir de forma más rigurosa a un registro como «una estructura que asocia un símbolo(la etiqueta) a una colección de pares símbolo-valor (nombre de campo y campo respectivamente)». En cuanto a la sintaxis, ocurre lo mismo que con los símbolos. Según del lenguaje que se trate (si los soportan, por supuesto), la forma de representarlos puede ser completamente distinta, más allá de que signifiquen lo mismo. En el caso de nuestro pseudocódigo, los representaremos indicando la etiqueta seguida del grupo de pares encerrado entre llaves ({}), tal como se muestra a continuación:

declare registro1 = @punto{@x: 100, @y: 100}

declare registro2 = @rectangulo{@bottom: 10, @left: 20, @top: 40, @right: 50, @color: @rojo}

Nota que cada uno de los pares se separa mediante comas, y que se utilizan los dos puntos (:) para delimitar al nombre de campo y el campo de cada par. Otra cosa que se debe destacar es que el orden en que escribamos los pares dentro del registro no tiene ninguna importancia. Es decir, no importa en qué orden se encuentren los pares, si dos registros tienen el mismo conjunto de pares, pero en un orden distinto, aún así se consideran equivalentes. Por ejemplo:

@punto1{@x: 100, @y: 100} == @punto2{@y: 100, @x: 100} //true

Adicionalmente, todos los nombres de campos siempre deben ser distintos. No se pueden emparejar dos campos distintos a un mismo símbolo en un mismo registro.

Bien, entonces ahora que ya sabemos qué son los registros y cómo los vamos a estar representando, podemos revisar algunas operaciones básicas que se pueden realizar con ellos. En realidad, son muchas las operaciones que un lenguaje puede ofrecer para trabajar con registros. Especialmente cuando se incluyen las operaciones ofrecidas en módulos aparte de la librería estándar de ese lenguaje. Aquí nos limitaremos a mencionar tan sólo las operaciones más comunes. Estas operaciones son el acceso a un campo determinado, el acceso al registro completo, obtener la etiqueta, determinar la longitud del registro, obtener una lista con los nombres de campos del registro, comparaciones de igualdad, y coincidencia de patrones. Nuevamente, todas estas operaciones se pueden especificar usando una sintaxis distinta dependiendo del lenguaje. La síntaxis que usaremos aquí para cada una de estas operaciones se puede revisar a continuación junto un ejemplo de uso:

R = @rectangulo{@bottom: 10, @left: 20, @top: 40, @right: 50, @color: @rojo}

//Acceso a un campo determinado mediante su nombre de campo
//operador punto (.)

Imprimir( R.@bottom + R.@top) //10 + 40 = 50
//Acceso a un registro completo
//Uso del identificador

Imprimir(R) //representación del registro completo
//Obtener la etiqueta de un registro
//propiedad etiqueta

Imprimir(R.etiqueta) //@rectangulo
//Determinar la longitud de registro (número de pares)
//propiedad long

Imprimir(R.long) // 5
//Obtener una lista con los nombres de campo
//propiedad fieldnames

Imprimir(R.fieldnames) // [@bottom, @left, @top, @right, @color]
//Comprobar igualdad

S = @cuadrado{@lado: 100, @color: @amarillo}

Imprimir (R == S) //false
//coincidencia de patrones
//uso junto a case

case R:
  of @rectangle{@bottom: A, @left: B, @top: C, @right: D, @color: E}

//Se obtienen 5 variables locales A = 10, B = 20, C = 40, D = 50, E = @amarillo

Nota: Como los campos pueden ser funciones, cuando accedamos a un registro con un nombre de campo emparejado con una función, podemos obtener valores diferentes de regreso. Esta característica puede resultar útil para definir registros con un comportamiento dinámico.

Revisitando el lenguaje kernel

Ahora que contamos con la idea de registro, podemos usarlos para simplificar el lenguaje kernel que definimos anteriormente. Esto debido a que podemos definir todos los tipos de datos compuestos y otros tipos de datos en términos de registros, y por lo tanto, contener un único tipo compuesto en el lenguaje kernel. Por ejemplo, podríamos definir a un átomo como un registro de longitud 0, es decir como una etiqueta sin pares asociados. También podríamos definir a una tupla como un caso especial de registro en el que los nombres de campo son enteros sucesivos que comienzan del 1 (ó 0). Y incluso podríamos definir las ya familiares listas como un tipo de dato recursivo construido con registros, donde cada registro (en una lista no vacía) consista de dos pares, uno con el nombre de campo @encabezado y otro con el nombre de campo @cola. De hecho, la sintaxis que hemos venido usado para trabajar con listas, se puede ver como un azúcar sintáctica para representar de forma simplificada esta idea.

lista.cola == lista.@cola

E|C == |{@encabezado: E, @cola: C} //un registro con etiqueta | y dos pares.

En conclusión, podríamos decir que es suficiente con este único dato compuesto (los registros) para describir y comprender la ejecución de un programa, pues todos los otros tipos de datos (listas, árboles y demás) se pueden codificar en torno a registros. Este hecho nos posibilita simplificar bastante el lenguaje kernel, sin con ello reducir su potencial.

Ejercicio
Escribe una función que tome una lista como entrada y retorne una versión de ella transformada a registro. Las listas de entrada incluirán siempre tres elementos: un primer elemento correspondiendo a la etiqueta y dos listas del mismo tamaño, la primera conteniendo los nombres de campo, y la segunda los campos. A i-ésimo nombre de campo de la primera lista le corresponde el i-ésimo campo de la segunda. Ejemplo:

[@z, [3 @a], [@b 5]] == @z{3:@a, @b: 5}

Ten en cuenta que si uno de los campos es una lista, esta tendrá este mismo formato, y también se debe transformar a registro.