2007-02-17

Orientação a objetos em Lua

Lua Dando continuidade a orientação a objetos em Perl, vamos agora falar sobre orientação a objetos em Lua.

Lua é uma linguagem de programação intrigante... sua orientação a tabelas (e metatabelas, metamétodos...) é prática e versátil.

Assim como quase tudo em Lua, a implementação de orientação a objetos também é baseada em tabelas e metatabelas.

Metatabelas


Metatabelas são tabelas que controla o comportamento de outras estruturas de dados.

Por exemplo, executar o seguinte no prompt do interpretador:
lua> nome = "La Batalema Pitonisto"
lua> print(nome:upper())
LA BATALEMA PITONISTO


De onde veio esse upper()? Daqui:
lua> print(getmetatable(nome))
table: 0x806b2a8


Ah! Há uma metatabela associada às strings!!!

Repare nisso:
lua> print(getmetatable(nome).__index == string)
true


Olha só! A chave __index da metatabela das strings é o módulo string!

Isto significa que:
nome:upper() == nome.upper(nome) == string.upper(nome)


Chave __index


A chave __index da metatabela pode ser uma tabela ou uma função e indica o que deve acontecer quando houver uma tentativa de leitura de uma chave que a estrutura de dados original não possua.

Por exemplo, o objeto referenciado pela variável nome (uma string) não possui a chave upper, então quando tentamos acessar esta chave, o sistema procurar pela chave na tabela referenciada pela chave __index da metatabela, que é string.

Se o valor da chave __index da metatabela for uma função, esta função deve ter dois argumentos: 1º o objeto original, 2º a chave; a função retorna o esperado para a chave.

Por exemplo, se queremos que uma tabela retorne o código ASCII do primeiro caráter do nome da chave informada, podemos usar uma função:
mt = {
    __index = function (t, k)
        return k:byte()
    end
}
var = setmetatable({}, mt)


Então, por exemplo, var.b retornará 98 (o código ASCII para b).

Esta chave é importantíssima para a orientação a objetos.

Chaves __newindex e __mode


Não vamos ver estas chaves neste artigo, mas é interessante conhecê-las.

A chave __newindex recebe uma função (argumentos: o objeto original, a chave, o valor atribuído) e é chamada quando se tenta atribuir um valor a uma chave que não existe. Sem esta chave da metatabela, a chave simplesmente seria criada no objeto e o valor atribuído a ela, com esta chave da metatabela, a função é executada e decide o que fazer.

A chave __mode decide se chaves e valores do objeto original serão referências fracas ou não. O padrão é não.

Classes e construtores


Em orientação a objetos, classe é um molde para a criação de novos objetos.

Em Lua, classe em geral é uma metatabela onde a chave __index aponta para ela própria. Algo assim:
mt = {}
mt.__index = mt


Isso faz com que as chaves da metatabela sejam padrões para as tabelas que a receberem como metatabela, ou seja, a metatabela se torna um molde para outras tabelas. As tabelas que fazem uso deste molde são chamadas instâncias.

As funções de uma classe/instância são chamadas métodos e sempre recebem implícita ou explicitamente como primeiro argumento a classe ou instância que faz a chamada.

Algumas linguagem são extremamente implícitas, escondendo a referência à classe ou instância.

Lua pode chamar um método passando a instância (ou classe) implícita ou explicitamente.

Exemplo de uma chamada explícita:
login = login.lower(login)


Agora a mesma chamada, mas passando a instância implicitamente:
login = login:lower()



Há um método especial chamado construtor, que é executado sempre que uma nova instância é criada.

Em algumas linguagens, o construtor é executado pela instância após esta ter sido criada. Nestas linguagens o construtor costuma ter o mesmo nome da classe (exceto Python, onde é chamado __init__()).

Em outras linguagens, o construtor é executado pela classe, como um método de classe, e retorna a instância. Nestas linguagens o construtor geralmente é chamado new() (como em Perl).

Lua usa a segunda abordagem.

Então um construtor simples pode ser:
function mt:new(o)
    o = o or {}
    return setmetatable(o, self)
end


O construtor aqui recebe como argumento uma tabela que servirá de referência para a criação da instância.

O primeiro comando garante que o argumento o é uma tabela, e o segundo associa a metatabela ao objeto, retornando-o.

Como new() é um método de classe, self representa a classe. Se fosse um método de instância (que deve ser chamado pela instância), self representaria a instância.

Outros métodos


Podemos criar outros métodos. Por exemplo, poderíamos querer que um somatório dos elementos numéricos da tabela seja retornado para o método soma():
function mt:soma()
    local s = 0
    table.foreachi(self, function (i, e)
        if type(e) == "number" then
            s = s + e
        end
    end)
    return s
end


Então podemos criar um objeto com alguns valores numéricos e retornar seu somatório:
var = mt:new { 2, 4, 6 }
ret = var:soma()


Aqui a variável ret receberá 12 (a soma dos elementos).

Metamétodos


Há alguns metamétodos interessantes, que agem junto a operadores.

Não vou entrar em detalhes, apenas vou citar alguns:
  • __add – gerencia operador de adição;
  • __sub – gerencia operador de subtração;
  • __mul – gerencia operador de multiplicação;
  • __div – gerencia operador de divisão;
  • __unm – gerencia operador unário de negação;
  • __eq – gerencia operador de igualdade;
  • __lt – gerencia operadores menor que e igual ou maior;
  • __le – gerencia operadores menor ou igual e maior que;
  • __pow – gerencia operador de potência;
  • __tostring – gerencia conversão para string;
  • __tonumber – gerencia conversão para número.


Herança


Imagine agora que queiramos aproveitar o que já programamos...

Queremos outra classe que além de devolver a soma, também devolva o produto, mas sem modificar a classe original.

Para isto herdamos uma nova classe. Mas como?

Muito simples: basta instanciar a classe pai normalmente, modificar a instância e usar esta instância como uma nova classe:
nmt = mt:new()

function nmt:produto()
    local p = 1
    table.foreachi(self, function (i, e)
        if type(e) == "number" then
            p = p * e
        end
    end)
    return p
end

var = nmt:new { 2, 4, 6 }
print(var:soma(), var:produto())


Serão exibidos 12 e 48.


Há uma forma mais avançada de herança, chamada herança múltipla, que acontece quando uma classe é herdeira de mais de uma classes pai.

Algumas linguagens, como Java, não permitem herança múltipla, devido a problemas de gerenciamento de polimorfismo, outras, como Perl e C++, confiam que o programador seja competente o suficiente para lidar com polimorfismos. Outras ainda, como Python, oferecem ferramentas para lidar com isso.

Em Lua, você escolhe!

A implementação de herança múltipla em Lua é complicada e mereceria um post por si só, portanto, quem quiser dar uma olhada, veja multiple inheritance no PiL.

Conclusão


Este foi apenas um artigo superficial sobre como implementar orientação a objetos em Lua. Espero que tenha sido elucidativo.

As melhores referências sobre Lua são o PiL e, preferencialmente, o PiL2.

[]'s