Bienvenidos sean a este post, como hemos visto anteriormente los metametodos para operadores arimeticos y relacionales pueden definir conductas para evitar situaciones erroneas, pero estas no cambian la conducta normal del lenguaje sino que Lua las ofrece como una manera para cambiar la conducta de las tablas para dos situaciones normales, el query y la modificacion de campos ausentes en una tabla.

Anuncios

El metametodo __index

Antes habiamos mencionado que al acceder a un campo ausente en una tabla devuelve un valor nil, esto es verdad pero no una verdad absoluta porque cuando accionas un acceso el interprete busca por un metametodo __index y si tal metodo no existe, como ocurre usualmente, luego el acceso devuelve un valor nil, de lo contrario el metametodo devolvera el resultado.

Nota: El ejemplo arquetipico aqui es de herencia.

Supongamos que queremos crear varias tablas que describen ventanas, y cada tabla debe describir muchos parametros de la ventana, tales como:

  • Posicion
  • Tamaño
  • Color
  • Esquema
  • y similares
Anuncios

Todos estos parametros tienen valores por defecto y si queremos construir ventanas dando solo los parametros no por defecto, una primera alternativa es con un constructor que completa los campos ausentes, una segunda alternativa es ordenar a la nueva ventana heredar cualquier campo ausente de una ventana prototipo, para ello primero declaramos el prototipo y una funcion constructor, la cual crea una nueva ventana compartiendo la metatabla:

Ventana = {}

Ventana.prototipo = { x=0, y=0, ancho=100, alto=100 }
Ventana.mt = {}

function Ventana.nuevo(o)
	setmetatable(o, Ventana.mt)
	return o
end

Nuestro siguiente paso sera definir al metametodo __index:

Ventana.mt.__index = function (tabla, clave)
	return Ventana.prototipo[clave]
end

Despues de agregar este codigo podemos intentar creando una nueva ventana y hacer una busqueda de uno de los campos ausentes:

> v = Ventana.nuevo{x = 10, y = 20}
> print(v.ancho)
100
>
Anuncios

Cuando Lua detecta que el v no posee el campo solicitado pero tiene una metatabla con un campo __index, el lenguaje llama al metametodo __index con los argumentos v (la tabla) y ancho (la clave ausente), el metametodo indexa el prototipo con la clave informada y devuelve el resultado, el uso del metametodo __index para herencia es un uso muy comun por lo que Lua provee un atajo, al margen del nombre el metametodo __index no necesita ser una funcion en su lugar puede ser una tabla, cuando es una funcion Lua lo llama con la la tabla y la clave ausente como sus argumentos como vimos anteriormente, en cambio cuando es una tabla Lua rehace el acceso en esta tabla, por lo tanto en nuestro ejemplo anterior podriamos haber declarado a mt.__index de la siguiente forma:

Ventana.mt.__index = Ventana.prototipo

Ahora cuando Lua busca por el campo __index de la metatabla, este encuentra el valor de Ventana.prototipo la cual es una tabla, como consecuencia Lua repite el acceso a esta tabla que equivaldria al siguiente codigo:

Ventana.prototipo["clave"]

Este acceso nos devolvera el resultado solicitado, el uso de una tabla como un metametodo __index provee una rapida y simple manera de implementar una simple herencia, una funcion aunque algo mas compleja provee mas flexibilidad porque nos permite implementar multiples herencias, cacheo y otras muchas variaciones, estas formas de herencia las discutiremos mas adelante, cuando queremos acceder una tabla sin invocar al metametodo __index usamos a la funcion rawget, la llamada a rawget(t, i) hace un acceso en crudo a la tabla t, que es un primitivo acceso sin considerar las metatablas, hacer este tipo de acceso no mejora la velocidad de tu codigo pero en algunas ocasiones es necesario pero de esto hablaremos mas adelante.

Anuncios

El metametodo __newindex

Este metodo hace las actualizaciones por la tabla tal como el metametodo anterior lo hace por el acceso,, cuando asignas un valor a un indice ausente en una tabla el interprete busca por un metametodo __newindex, si hay uno el interprete lo llama en lugar de hacer la asignacion, al igual que __index si el metametodo es una tabla, el interprete hace la asignacion en esta tabla en lugar de la original, mas aun hay una funcion de tipo raw que te permite esquivar el metametodo , la llamada rawset(t, k, v) setea el valor de v asociado con la clave (k) en la tabla t sin invocar ningun metametodo.

El uso combinado de los metametodos __index y __newindex permite varias poderosas construcciones en Lua, tal como las tablas de solo lectura, tablas con valores predeterminados y herencia por programacion orientados a objetos, los dos primeros los veremos en el proximo post pero el tercero tendra su propio post, antes de terminar veamos el codigo final de nuestro programa:

metamtd02.lua

Ventana = {}

Ventana.prototipo = { x=0, y=0, ancho=100, alto=100 }
Ventana.mt = {}

function Ventana.nuevo(o)
	setmetatable(o, Ventana.mt)
	return o
end

Ventana.mt.__index = Ventana.prototipo
Anuncios

Tablas con valores por defecto

El valor por defecto de cualquier campo en una tabla comun es nil pero es facil cambiar este valor por defecto con metatablas, veamos el siguiente ejemplo:

function setDefault(t, d)
	local mt={__index = function() return d end }
	setmetatable(t, mt)
end

En este caso tendremos dos argumentos, uno llamado t para recibir la tabla y d para el valor por defecto, luego crearemos una tabla local llamada mt, en este caso usaremos a __index y le asignaremos una funcion que devolvera el valor de d, por ultimo usaremos a setmetatable y pasaremos el valor de t y de mt, con esto explicado apliquemos un ejemplo:

> tab = { x=10, y=20 }
> print(tab.x, tab.z)
10      nil
> setDefault(tab, 0)
> print(tab.x, tab.z)
10      0
>

En este caso creamos una tabla llamada tab con dos valores (x e y), si pedimos imprimir el valor de x y z de tab nos devolvera el valor de x y un valor nil para z porque no hemos seteado ninguno, si usamos a setDefault, le enviamos a tab y el valor por defecto de 0, si volvemos a repetir la operacion anterior ahora para z existe un valor de 0 porque es el valor que informamos para asignar por defecto.

Anuncios

La funcion setDefault crea una nueva metatabla por cada tabla que necesita un valor por defecto, esto puede perjudicarnos si tenemos muchas tablas sin embargo la metatabla tiene el valor por defecto d enlazado dentro de su metametodo, asi la funcion no puede usar una simple metatabla para todas las tablas, para permitir el uso de una simple metatabla para tablas con diferentes valores por defecto podemos almacenar el valor por defecto en la misma tabla usando un campo exclusivo, si no estamos preocupados por los conflictos de nombres podemos usar una clave como “___” para nuestro campo exclusivo, veamos un caso:

local mt={__index = function(t) return t.___ end }
function setDefault(t, d)
	t.___ = d
	setmetatable(t, mt)
end

Pero si nos preocupa sobre el conflicto de nombres es facil asegurar la exclusividad de esta clave especial, lo unico que necesitamos es crear una tabla y usarla como la clave:

local clave = {}
local mt={__index = function() return t[clave] end }
function setDefault(t, d)
	t[clave] = d
	setmetatable(t, mt)
end

Un enfoque alternativo para asociar cada tabla con su valor por defecto es usar una tabla separada donde los indices son las tablas y los valores son sus valores por defecto, sin embargo para la correcta implementacion de este enfoque necesitamos un tipo especial de tabla llamado tablas debiles (weak tables) y por ahora no las usaremos pero mas adelante lo explicaremos para su implementacion.

Anuncios

Otra alternativa es memorizar las metatablas en orden para reusar la misma metatabla para tablas con el mismo valor por defecto, sin embargo esto tambien necesita tablas debiles por lo tanto hablaremos de esto mas adelante.

Seguimiento de accesos a la tabla

Tanto __index como __newindex son relevantes solo cuando el indice no existe en la tabla, de la unica manera que podemos agarrar todos los accesos a una tabla es manteniendola vacia, asi que si queremos monitorear todos los accesos a una tabla deberiamos crear un proxy para la tabla real, este proxy es una tabla vacia con los apropiados metametodos __index y __newindex que sigue todos los accesos y los redirecciona a la tabla original, supongamos que t es la tabla original que queremos seguir, usaremos un codigo similar a este:

t = {}

local _t = t

t = {}

local mt = {
	__index = function(t, k)
		print("*Acceso al elemento " .. tostring(k))
		return _t[k]
	end,

	__newindex = function(t, k, v)
		print("*Actualizacion de elemento " .. tostring(k) ..
			" hacia " .. tostring(v))
		_t[k] = v
	end
}
setmetatable(t, mt)

Primero crearemos una tabla llamada t, nuestro siguiente paso sera crear un acceso privado a la tabla t, por medio de _t, luego crearemos el proxy que es vaciar nuevamente a t, nuestro siguiente paso sera crear la metatabla llamada mt, en esta definiremos a __index que nos mostrara un mensaje y accedera a la tabla original, nuestra siguiente definicion sera para __newindex donde mostraremos un mensaje y asignaremos el valor en v a _t con la posicion en k, por ultimo usaremos a setmetatable para asignarle a la tabla a mt, hagamos una prueba:

> t[2] = "Hola"
*Actualizacion de elemento 2 hacia Hola
> print(t[2])
*Acceso al elemento 2
Hola
>

El defecto mas importante con este esquema es que no permite atravesar las tablas, la funcion pairs operara sobre el proxy y no sobre la tabla original.

Anuncios

Pero y si queremos monitorear varias tablas, no necesitamos una metatabla diferente para cada una de ellas, en su lugar podemos asociar de alguna forma cada proxy a su tabla original y compartir una metatabla comun para todos los proxies, este problema es muy similar a la asociacion de las tablas con sus valores por defecto, de la cual hablamos anteriormente, un ejemplo seria mantener la tabla original un campo de proxy usando una clave exclusiva, veamos el siguiente codigo:

local indice = {}

local mt = {
	__index = function(t, k)
		print("*Acceso al elemento " .. tostring(k))
		return t[indice][k]
	end,

	__newindex = function(t, k, v)
		print("*Actualizacion de elemento " .. tostring(k) ..
			" hacia " .. tostring(v))
		t[indice][k] = v
	end
}

function seguir(t)
	local proxy = {}
	proxy[indice] = t
	setmetatable(proxy, mt)
	return proxy
end

En este caso trabaja de forma similar a la anterior pero quitamos la copia de la tabla, _t, y usamos a indice, tampoco creamos dos veces a la tabla t, nuestra siguiente modificacion sera a la hora de definir los metametodos donde reemplazaremos a _t[k] por t[indice][k], en este caso usaremos a la tabla directamente por medio de indice, por ultimo usaremos una funcion en lugar de directamente setmetatable, en este caso crea una tabla llamada proxy, agrega al proxy en la posicion informada por indice a la tabla t, luego usa a setmetatable con proxy y mt por ultimo devuelve a proxy, lo bueno de esta modificacion es que a partir de ahora para hacer un seguimiento solo debemos usar t=seguir(t)

Tablas de solo-lectura

Es facil adaptar el concepto de proxies para implementar tablas de solo-lectura, todo lo que debemos hacer es generar un error cuando sigamos cualquier intento de modificar la tabla, para el metametodo __index podemos usar una tabla, la misma tabla original, en lugar de una funcion y como no necesitamos hacer un seguimiento de las busquedas porque es mas simple y mas eficiente redireccionar todas las busquedas a la tabla original, este uso sin embargo demanda una nueva metatabla por cada proxy de solo-lectura, veamos el siguiente codigo:

Anuncios
function soloLectura(t)
	local proxy = {}
	local mt = {
		__index = t,
		__newindex = function(t, k, v)
			error("Intentas actualizar una tabla de solo-lectura", 2)
		end
	}
	setmetatable(proxy, mt)
	return proxy
end

En este caso tendremos la funcion que recibe una tabla como argumento, luego crearemos una tabla local llamada proxy, en la metatabla a __index le asignaremos el valor del argumento, para __newindex usaremos una funcion donde generara una error indicando que no se puede actualizar/modificar, luego usaremos a setmetatable para asignarle la metatabla, en el ultimo paso devolveremos a proxy, pongamos esto en practica:

> dias = soloLectura{"Domingo","Lunes","Martes","Miercoles","Jueves",
>> "Viernes","Sabado"}
> print(dias[2])
Lunes
> dias[2] = "lalala"
stdin:1: Intentas actualizar una tabla de solo-lectura

En este caso usaremos los dias de la semana, si lo implementamos con la funcion, al usar el print en la tabla dias nos devuelve los valores sin ningun inconveniente, en cambio cuando queremos reemplazar alguno de sus valores nos devuelve el mensaje de error.

Anuncios

En resumen, hoy hemos visto todos los metametodos de acceso a tablas, como son, para que se usan, como puden facilitarnos o restringirnos el acceso a las tablas, espero les haya sido util sigueme en Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Tengo un Patreon donde podes acceder de manera exclusiva a material para este blog antes de ser publicado, sigue los pasos del link para saber como.

Tambien podes donar

Es para mantenimiento del sitio, gracias!

$1.00