Comparação entre Lua e JavaScript

Sexta-feira assisti a uma palestra sobre a linguagem Lua. Enquanto assistia, não pude deixar de notar a existência de paralelos entre as tabelas de Lua e os objetos de Javascript. Este artigo não procura ilustrar todas as diferenças entre as linguagens, apenas as que se referem a tabelas, objetos e a orientação a objetos a partir de protótipos.

Antes de continuar, apenas um aviso: durante o texto, utilizarei as palavras tabela e objeto intercambiavelmente. Cabe ao leitor saber que o termo técnico correto é objeto em JavaScript e tabela em Lua. Quando realmente for necessário fazer uma diferenciação, utilizarei os termos com inicial em maiúscula e em inglês.

Tabelas, arrays e objetos

Nas duas linguagens se cria um objeto vazio com {}. Além disso, por padrão, toda variável é global nas duas linguagens, a não ser que se especifique o contrário. Isso é feito através da palavra-chave local em Lua e var em JavaScript.

-- Lua
variavelGlobal = {}
local variavelLocal = {}

// Javascript
variavelGlobal = {}
var variavelLocal = {}

Criada a tabela, podemos acessar e criar novos campos através da notação de records ou de arrays:

-- Lua
a = {}
a.x = 5
a["y"] = 10

// JavaScript
a = {}
a.x = 5
a["y"] = 10

Outra forma de criar campos em um objeto é já especificá-los na sua criação. Aqui, a sintaxe muda apenas sutilmente:

-- Lua
b = { x = 5, y = 10 }

// JavaScript
b = { x: 5, y: 10 }

Por outro lado, Tables em Lua possuem algumas das características de Arrays em JavaScript. Novamente, existem diferenças sutis entre as linguagens. Primeiramente, em Lua tabelas-array são inicializadas com chaves, como qualquer Table, enquanto que em JavaScript Arrays (que herdam de Object) são inicializados com colchetes. Outra diferença é um açúcar sintático que não existe (que eu saiba) em JavaScript, mas que existe em Lua e que nos permite já criar um campo na sua inicialização. Por fim, talvez a diferença mais chata seja que arrays em Lua são indexados a partir de 1. Sabendo disso, podemos enumerar os dias da semana e definir que dia é hoje da seguinte forma:

-- Lua
semana = { "Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"; hoje = "Sábado" }

// JavaScript
semana = [ "Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado" ]
semana.hoje = "Sábado"

Nas duas linguagens, podemos escolher iterar apenas na parte array ou iterar por todos os campos do objeto. Isso é feito da seguinte forma:

-- Lua: apenas array
for i, v in ipairs(semana) do
    print(i, v)
end

-- Lua: tudo
for i, v in pairs(semana) do
    print(i, v)
end

// JavaScript: apenas array
for(var i = 0 ; i < semana.length; i++) {
    console.log(i + "\t" + semana[i])
}

// JavaScript: tudo
for(var i in semana) {
    console.log(i + "\t" + semana[i])
}

Metatabelas e Protótipos

Aqui, o como-fazer começa a se diferenciar entre as duas linguagens; por isso, tratá-las-ei separadamente, começando pelo JavaScript. No entanto, o como-funciona guarda muitas semelhanças, que irei resgatar quando chegar à explicação sobre Lua.

Em JavaScript, se quisermos alterar o estado de um objeto através de um método, usamos a palavra-chave this.

var obj = { value: 0, inc: function () { this.value += 1 } }
obj.inc()

No entanto, a notação para se criar um novo objeto usando chaves funciona bem quando se está criando um singleton. Quando se quer um objeto que servirá de base para outros, de forma similar a uma classe, o melhor é usar a notação abaixo.

// JavaScript
function Shape(color) {
    this.color = color || "none"
    this.printColor = function () { console.log(this.color) }
}

var s1 = new Shape()
var s2 = new Shape("red")
s2.printColor()

Feito isso, se quisermos criar uma nova classe que herda comportamento da classe Shape, usamos a palavra prototype.

// JavaScript
function Square(color, size) {
    this.color = color || "none"
    this.size = size || 1
    this.getArea = function () { return this.size * this.size }
}
Square.prototype = new Shape()
var s = new Square("blue", 5)
s.getArea()
s.printColor()

O que está acontecendo aqui? Embora não tenha sido definida uma função printColor na classe Square, uma função é chamada, e, curiosamente, essa função faz exatamente o que se esperava que fizesse. A mágica está no uso de prototype – quando o interpretador do JavaScript percebe que não existe um campo (seja ele uma variável ou uma função) em um objeto, ele procura em seu protótipo. Se essa variável não for encontrada no protótipo, ela será procurada no protótipo do protótipo. Esse processo continuará indefinidamente até que a variável efetivamente seja encontrada ou até que não existam mais protótipos onde se procurar.

// JavaScript
function WeightedSquare(color, size, weight) {
    this.color = color || "none"
    this.size = size || 1
    this.weight = weight || 0
    this.getWeight = function () { return this.weight }
}
WeightedSquare.prototype = new Square()
w = new WeightedSquare("green", 3, 2)
w.getArea()
w.printColor()

O problema desse modo de orientação a objetos é que ele não cria um encapsulamento das variáveis. Assim, w.color = "purple" funcionará perfeitamente.

Em Lua, podemos fazer as mesmas coisas de modo similar. No entanto, como Tables são abstrações mais genéricas que Objects (já que, por exemplo, podem cumprir o papel de um Array também), às vezes precisamos de um ou dois passos a mais.

Criar um singleton pode ser feito de forma muito similar, com a diferença de que a referência a si mesmo é explícita em Lua, e se chama self em vez de this. Há, no entanto, um açúcar sintático para isso, também mostrado abaixo.

-- Lua
obj = { value = 0, inc = function (self) self.value = self.value + 1 end }
obj.inc(obj)

-- Lua com açúcar sintático
obj = { value = 0 }
function obj:inc() self.value = self.value + 1 end
obj:inc()

Novamente, se quisermos usar um objeto como base para outros objetos, precisaremos de uma outra notação. Aqui também já aparece o primeiro uso da ideia de metatabelas e metamétodos. Assim como Tables são mais genéricas que Objects, Metatables são mais genéricas que Prototypes. Por um lado, isso significa que usar Tables e Metatables como Objects e Prototypes dá um pouco mais de trabalho; por outro, isso significa que Tables e Metatables têm mais casos de uso (embora não serão mostrados aqui).

-- Lua
Shape = { color = "none" }
function Shape:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function Shape:printColor() print(self.color) end

local s1 = Shape:new()
local s2 = Shape:new({ color = "red" })

s2:printColor()

Como isso funciona? Primeiro, é criada uma Table que servirá de base para os objetos da classe Shape. Depois, é criado um novo método, chamado new, que usa uma Table passada como argumento como base ou cria uma nova. Esse método também estabelece que a metatabela desse objeto o é a classe Shape e que o metamétodo __index dessa metatabela é a própria classe Shape. O papel desse metamétodo __index é ditar o que acontece quando se tenta acessar um índice inexistente; se for utilizada uma tabela em vez de uma função, o interpretador procurará a variável que não foi encontrada nessa tabela.

Portanto, quando é chamado o método s2:printColor(), o interpretador percebe que não há nenhum campo na tabela s2 com esse nome; então ele procura na metatabela associada a ela a definição de __index para saber o que fazer. Ele encontra a definição, que diz que o interpretador deve continuar procurando na tabela Shape (que era o self quando Shape:new foi chamado) e finalmente encontra o que estava procurando.

Percebe-se que esse processo é muito similar ao que acontece quando chamamos um método que não existe em um Object em JavaScript, mas que existe em seu Prototype. A grande diferença é que esse processo já se inicia logo na primeira classe do processo, e não somente quando começamos a usar herança.

-- Lua
Square = { size = 1 }
setmetatable(Square, Shape)
function Square:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end
function Square:getArea() return self.size * self.size end
s = Square:new({color = "red", size = 5})

A herança em si não acrescenta nada de novo, exceto que, assim como protótipos em JavaScript, o processo de busca por uma variável pode continuar a busca na metatabela da metatabela e assim sucessivamente.

-- Lua
WeightedSquare = { weight = 0 }
setmetatable(WeightedSquare, Square)
function WeightedSquare:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end
function WeightedSquare:getWeight() return self.weight end
w = WeightedSquare:new({ color = "green" })
w:printColor()

Novamente, o problema desse método é que ele não cria um encapsulamento. Podemos, assim como em JavaScript, alterar w.color = "purple" sem problemas.

No entanto, diferentemente do que acontece com JavaScript, é possível criar um sistema de herança múltipla em Lua. Basta que o metamétodo __index seja efetivamente uma função, em vez de uma tabela, e que essa função percorra uma tabela de tabelas procurando uma que contenha o método necessário. Exemplo de classes que façam isso ficam como exercício para o leitor (ou então leiam o capítulo 16.3 de Programando em Lua).

Encapsulamento

Antes de começarmos com o encapsulamento, vejamos que ambas linguagens suportam closures:

-- Lua
function makeInc(init, step)
    local state = init or 0
    local inc = step or 1
    return function()
        state = state + inc
        return state
    end
end

//JavaScript
function makeInc(init, step) {
    var state = init || 0
    var inc = step || 1
    return function () {
        state = state + inc
        return state
    }
}

Em JavaScript, o uso de closures mais o uso de singletons dá origem ao Module Pattern. Esse padrão pode ser facilmente traduzido para Lua.

-- Lua
function newAccount (initialBalance)
    local self = {balance = initialBalance}
    
    local withdraw = function (v)
                         self.balance = self.balance - v
                     end
    
    local deposit = function (v)
                        self.balance = self.balance + v
                    end
    
    local getBalance = function () return self.balance end
    
    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end

// JavaScript
function newAccount(initialBalance) {
    
    return {
        withdraw: withdraw,
        deposit: deposit,
        getBalance: getBalance
}

(Exemplo retirado do capítulo 16.4 do livro Programming in Lua). A diferença de se usar esse padrão em Lua é que, apesar de estarmos lidando com objetos com estado, esse estado está contido na closure, e não mais sendo passado como argumento. Portanto, para chamar esses métodos usamos ponto e não dois-pontos.

Publicidade

2 Respostas para “Comparação entre Lua e JavaScript

  1. jonathanrz agosto 28, 2011 às 2:47 pm

    Muito bom o artigo, parabéns.

    Não li tudo porque é muito extenso, mas até a parte que li, estava muito bom.

  2. pedro agosto 14, 2012 às 12:03 pm

    Muito bacana, obrigado!
    Deu para entender melhor como fazer a herança em lua que eu buscava a um tempo, apesar de não ser o mais prático e natural gostei do uso do método setmetatable.
    Agora usando closures parece mais natural para criar objetos, não é uma boa prática, não é possível fazer heranças a partir disso?
    Abs,
    Pedro

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

%d blogueiros gostam disto: