2006-08-15

Números complexos em Lua

Lua é uma poderosa linguagem de programação procedural, orientada a objetos, de tipagem dinâmica, baseada em tabelas associativas e semântica extensível, projetada e implementada no Tecgraf, Grupo de Computação Gráfica da PUC-Rio, em 1993.

Foi projetada para ser uma linguagem de extensão, para que fosse possível que os usuários reconfigurassem aplicações sem necessidade de recompilação das aplicações.

Atualmente é a linguagem de script mais usada para programação de jogos (em segundo lugar está Python).

Uma curiosidade de Lua é não haver um módulo de suporte a números complexos. Vamos então implementar um.

Metatabelas



Em orientação a objetos, o «tipo» de objeto é chamado classe. Lua trabalha com um conceito ligeiramente diferente: metatabelas e metamétodos.

Em Lua, os tipos definidos pelo usuário são sempre tabelas, cujos elementos podem ser quaisquer objetos de primeira ordem, inclusive funções (métodos) e outras tabelas.

Ainda é possível associar uma metatabela a uma tabela: esta metatabela define como a tabela deve comportar-se. Então a metatabela funciona de forma semelhante a classe em outras linguagens.

Os métodos da metatabela definem como a tabela reagirá às operações matemáticas, às comparações e até como responderá quando for solicitado o valor de um elemento elemento que não possui (podemos chamar isso de valores default).

Para as operações matemáticas temos os métodos __add() (adição), __sub() (subtração), __mul() (multiplicação), __div() (divisão) e __pow() (potência). Ainda há os métodos __unm() (inversão de sinal), __tostring() (conversão para string) e __concat() (concatenação).

As operações comparativas são __eq() (igualdade), __lt() (menor que) e __le() (menor ou igual). Alguém pode perguntar «e quanto a ‘maior que’ e ‘maior ou igual’?». A resposta é simples, se a > b então b < a, capicci? A mesma idéia é para diferença (a ~= b é o mesmo que not a == b).

Está confuso até aqui? Vai clarear assim que começarmos a criar nossa metatabela. Vamos então à criação de nossa metatabela: primeiro usaremos o construtor de tabela ({}) para criar a metatabela com alguns valores default e em seguida criaremos o construtor próprio para números complexos:

local cmt = { real = 0, img = 1 }
cmt.__index = cmt

function cmt:new(o)
o = o or {}
setmetatable(o, self)
return o
end


Números complexos são formados por duas partes, uma real e uma imagem, então criamos nossa metatabela assim.

A sintaxe cmt:new(o) é um açúcar sintático para cmt.new(self, o) e é usada para métodos.

O comando setmetatable(o, self) define que a metatabela de o será self e o elemento __index informa de onde a tabela deve retirar os valores default (para elementos não definidos), e será a própria metatabela.


No entanto queremos ter alguma flexibilidade ao criar um número complexo:
  • Se não for passado argumento, queremos que a função retorne i;
  • Se for passado um número (real), queremos retornar ele mesmo;
  • Se for passado um número complexo, queremos retornar uma cópia dele;
  • Se forem passados dois números (reais), queremos que o primeiro seja a parte real e que o segundo seja a imagem (se a imagem for zero, retorne somente a parte real).


Assim sendo, podemos criar a seguinte função:

local function new(...)
if arg.n == 0 then
-- nenhum argumento retorna i
return cmt:new { real = 0, img = 1 }
elseif arg.n == 1 and type(arg[1]) == “number” then
-- um argumento: numero real
return arg[1]
elseif arg.n == 1 and getmetatable(arg[1]) == cmt then
-- um argumento: numero complexo
return cmt:new { real = arg[1].real, img = arg[1].img }
elseif arg.n == 2 and
type(arg[1]) == “number” and type(arg[2]) == “number” then
-- dois argumentos reais
if arg[2] == 0 then
return arg[1]
else
return cmt:new { real = arg[1], img = arg[2] }
end
else
error “parse error”
end
end


Mostrando nosso número complexo



O primeiro método que definiremos será para exibir nosso número complexo.

Sem este método, o comando abaixo retornaria assim:

lua> print(numero)
table: 0x80725e0


E queremos que, na verdade retorne algo do tipo:

lua> print(numero)
3 + 2i


Então precisamos definir o método __tostring(). Mas não será tão fácil assim!

Imagine só: precisamos definir pelo menos oito casos diferentes:
  1. imagem = 0
  2. real = 0, imagem = 1
  3. real = 0, imagem = -1
  4. real ≠ 0, imagem = 1
  5. real ≠ 0, imagem = -1
  6. real = 0, imagem ≠ 0
  7. real ≠ 0, imagem > 0 e imagem ≠ 1
  8. real ≠ 0, imagem < 0 e imagem ≠ -1


Vamos então!

function cmt:__tostring()
local a, b = self.real, self.img
if b == 0 then
return tostring(a)
elseif b == 1 and a == 0 then
return “i”
elseif b == -1 and a == 0 then
return “-i”
elseif b == 1 and a ~= 0 then
return a .. “ + i”
elseif b == -1 and a ~= 0 then
return a .. “ - i”
elseif b ~= 0 and a == 0 then
return b .. “i”
elseif b > 0 and a ~= 0 then
return a .. “ + “ .. b .. “i”
elseif b < 0 and a ~= 0 then
return a .. “ - “ .. (-b) .. “i”
else
error “unexpected (a + bi) combination: contact author”
end
end


Uma funçãozinha útil



Para definir alguns parâmetros importantes precisamos definir o que acontece com o número quando tentamos invertê-lo ((a + bi)-1).

A operação matemática é multiplicar a fração resultante por uma expressão equivalente a 1, como (a – bi) / (a – bi).

Fazendo esta continha simpática, obtemos real a / (a2 + b2) e imagem -b / (a2 + b2).

Vamos criar nossa função:

local function inv(v)
if getmetatable(v) ~= cmt then
return v ^ (-1)
else
local a, b, q = v.real, v.img
q = a ^ 2 + b ^ 2
return new(a / q, -b / q)
end
end


Nesta função, a primeira coisa que fizemos foi verificar se o argumento é um número complexo. Se não for, a função retorna um dividido pelo argumento. Se for um número complexo, realiza o cálculo citado.

Repare na linha:

local a, b, q = v.real, v.img


Neste comando, a, b e q são definidos como variáveis locais. a recebe v.real, b recebe v.img e q recebe nil.

Inversão de sinais



Se temos um número complexo b, queremos que -b retorne um número complexo com os sinais do real e da imagem invertidos:

function cmt:__unm()
return new(-self.real, -self.img)
end


Igualdade



Vamos verificar igualdade. Quando Lua verifica a == b, sendo a e b tabelas, na verdade está verificando se a e b são exatamente a mesma tabela, não se seus elementos são iguais. O quer queremos quando comparamos dois números complexos é se representam o mesmo valor, ou seja, se seus reais são iguais e se suas imagens também são.

Precisamos então criar um método para tratar isso:

function cmt:__eq(v)
if getmetatable(v) == cmt then
return self.real == v.real and self.img == v.img
else
return self.img == 0 and v == self.real
end
end


Operações binárias



Agora definiremos as operações binárias, ou seja, que necessitam de dois operandos: adição (+), subtração (-), multiplicação (*), divisão (/), potência (^) e concatenação (..).

Em todos os casos verificaremos se o segundo elemento da operação é também um número complexo ou não.

  • Adição

function cmt:__add(v)
local a1, b1, a2, b2, a3, b3 = self.real, self.img
if getmetatable(v) ~= cmt then
a2, b2 = v, 0
else
a2, b2 = v.real, v.img
end
a3, b3 = a1 + a2, b1 + b2
return new(a3, b3)
end


Na declaração das variáveis locais, a1 recebe self.real e b1 self.img. Todas as demais variáveis recebem nil.

  • Subtração (nada mais do que soma com o sinal invertido)

function cmt:__sub(v)
return self + (-v)
end


  • Multiplicação (muito semelhante à adição)

function cmt:__mut(v)
local a1, b1, a2, b2, a3, b3 = self.real, self.img
if getmetatable(v) ~= cmt then
a2, b2 = v, 0
else
a2, b2 = v.real, v.img
end
a3, b3 = a1 * a2 – b1 * 2, a1 * b2 + a2 * b1
return new(a3, b3)
end


  • Divisão (divisão é a multiplicação onde o segundo termo é invertido)

function cmt:__div(v)
return self * inv(v)
end


  • Potência (aqui trataremos apenas expoentes inteiros, que não passam de multiplicações sucessivas)

function cmt:__pow(v)
if v == 0 then
-- expoente 0 retorna 1
return 1
elseif v == 1 then
-- expoente 1 retorna uma cópia de si mesmo
return new(self)
elseif v < 0 then
-- expoente negativo retorna inversao
return inv(self ^ (-v))
else
local aux, cont = new(self)
for cont = 2, v do
aux = self * aux
end
return aux
end
end


Repare a recursividade na linha:

return inv(self ^ (-v))


A potência chama novamente __pow() para self, mas desta vez com argumento -v.

  • Concatenação (fácil: retorna a contenação das strings!)

function cmt:__concat(v)
return tostring(self) .. tostring(v)
end


Vamos tornar isso tudo útil?



Até agora está tudo muito bonito, tratando números complexos e tudo mais... mas números complexos só são úteis se pudermos fazer duas coisas: 1converter números reais em complexos quando tentamos extrair a raiz de um número negativo e 2converter números complexos para reais por meio das operações básicas.

Bem, a segunda coisa nosso módulo já faz, falta a primeira! Para tanto, vamos criar uma função de raiz quadrada segura, que retorne um número complexo quando a base for negativa:

local function sqrt(v)
if type(v) ~= “number” then
error “value must be a number”
end
if v >= 0 then
return v ^ .5
else
return (0, (-v) ^ .5)
end
end


Também será útil termos uma função que retorne verdadeiro ou falso para verificar se um valor é um número complexo:

local function iscomplex(v)
return getmetatable(v) == cmt
end


Finalizando



Até aqui, tudo o que fizemos foi privado (local). Vamos então criar uma tabela pública onde colocaremos somente as partes que nos interessam:

complex = {
iscomplex = iscomplex,
new = new,
sqrt = sqrt,
i = new()
}


Vamos agora testar! Salve este arquivo como complex.lua no diretório atual e execute o interpretador lua. Execute os seguintes comandos e veja se funciona:

lua> require “complex”
lua> for c = -4, 7 do print(c, complex.i ^ c) end
-4 1
-3 i
-2 -1
-1 -i
0 1
1 i
2 -1
3 -i
4 1
5 i
6 -1
7 -i
lua> a = complex.sqrt(-9) + 2
lua> b = complex.new(3, 2)
lua> print(a, b)
2 + 3i 3 + 2i
lua> print(a + b)
5 + 5i
lua> print(a * b)
13i
lua> print(a / 2)
1 + 1.5i
lua> print(b ^ 2)
5 + 12i


Se tudo sair direitinho, parabéns! Acabou de fazer seu primeiro módulo de números complexos.

[]'s