Lu-a-Bá - O bê-a-bá de Lua para a comunidade lusófona

Glossário

Como se espera de um documento intitulado “Glossário”, o objetivo deste documento é esclarecer o significado de alguns termos relevantes para esta área de conhecimento, isto é, sobre a linguagem Lua e sua implementação. A quantidade de termos que interferem na compreensão deste tema não é pequena, e portanto serão priorizados os termos que:

  1. São específicos deste ambiente de desenvolvimento;
  2. São pouco comuns entre outros ambientes de desenvolvimento;
  3. São comuns, porém possuem um significado diferente neste ambiente.

Vale lembrar que o glossário contempla apenas as versões abrangidas pelo projeto Lu-a-Bá. Referências à documentação são adicionadas para cada termo abordado, dando preferência a versões traduzidas para português, quando possível (no momento, apenas 5.1 e 5.2).

Os termos a seguir estão em ordem alfabética. Vide lista completa no conteúdo desta página.

Açúcar sintático

O termo açúcar sintático é relativamente comum na documentação de Lua, portanto vale explicar o que ele significa. Um açúcar sintático é uma expressão sinônima de outra, que de algum modo pode ser mais conveniente ou mais fácil de entender em algumas situações.

Exemplo:

-- a expressão ...
function a() return 1 end
-- ... é um açúcar sintático para
a = function() return 1 end

t.a -- açúcar sintático para t["a"]

Como se pode observar, a expressão chamada de açúcar sintático costuma ser mais intuitiva ou mais enxuta, embora possua o mesmo significado de uma outra expressão menos intuitiva ou mais verbosa.

Ambiente

Este é um conceito que passou por uma mudança mais brusca, da versão 5.1 para a 5.2, tendo se mantido o mesmo desde então.

Nas versões mais atuais, um ambiente é uma variável local externa cujo nome é _ENV e o tipo é uma tabela. Todos os elementos dessa tabela são considerados parte do ambiente, portanto, e na prática uma variável global é um elemento do ambiente ativo.

Com essa definição de ambientes, trocar de ambiente resume-se a substituir o valor da variável _ENV por uma tabela diferente. Ao fazer isso, os elementos da nova tabela é que serão as variáveis globais disponíveis. Atribuir uma tabela vazia a _ENV, portanto, significa esvaziar totalmente o ambiente.

Na versão 5.1, ambientes também são tabelas, porém não são identificadas pelo nome _ENV, e não é possível alterar o ambiente vigente de um modo genérico (uma única alteração para várias funções de uma vez, por exemplo). É preciso especificar para qual objeto o ambiente novo será usado.

O acesso a ambientes, neste caso, é intermediado por funções específicas (getfenv() para obter o ambiente vigente, e setfenv() para configurar um novo ambiente para uma função ou co-rotina).

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Ambiente global

Diferentemente do conceito de ambiente, o de ambiente global se manteve mais estável ao longo das versões. Trata-se de uma tabela mantida internamente pela biblioteca Lua e cujo valor pode ser acessado em código Lua pela variável _G.

É fácil confundir as variáveis _ENV e _G, já que por padrão elas guardam exatamente o mesmo valor. Mas cabe lembrar a seguinte distinção:

Portanto a própria variável _G consta na tabela apontada por _G, ao passo que _ENV, não sendo uma variável global, não consta.

O script a seguir pode ser executado em versões a partir da 5.2 para demonstrar isto.

print (_G._G == _G)   --> true
print (_ENV == _G)    --> true
print (_ENV._G)       --> 0x7f6008a58f00
                      --  (ou qualquer outra identificação de
                      --  tabela, varia com a execução)
print (_G._ENV)       --> nil

Podemos adaptá-lo para Lua 5.1 da seguinte forma (omitindo a última linha, já que não há _ENV neste caso):

print (_G._G == _G)     --> true
print (getfenv() == _G) --> true
print (getfenv()._G)    --> 0x7f6008a58f00
                        --  (ou qualquer outra identificação de
                        --  tabela, varia com a execução)

Independente da versão de Lua, alterar o valor da variável _G não resulta em qualquer consequência para a execução do programa (salvo quando o programa de algum modo faz referência a esta variável).

Exemplo:

teste = "Ainda estou aqui"
_G = nil
print(teste) --> Ainda estou aqui

Isto ocorre pois a variável apenas mudou seu apontamento, mas o ambiente vigente permanece o mesmo, e é dele que qualquer variável global será lida.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

API C

Lua é uma linguagem feita para executar embutida em outros programas (ainda que o programa em questão seja o interpretador lua e toda a lógica de negócio seja implementada em Lua). Deste modo, precisa de uma interface com o programa hospedeiro. Tal interface é a API C.

Por meio dela é possível criar uma interação entre o programa hospedeiro e um ou mais estados Lua. A interação é feita com o auxílio de uma pilha virtual, de modo que todas as funções da API interagem com uma pilha desse tipo.

As funções e tipos da API são declaradas no arquivo lua.h, e tem a convenção de iniciar com o prefixo lua_ (por exemplo, lua_newstate() e lua_pushnumber()). Os tipos definidos neste cabeçalho tem a convenção de iniciar por uma letra maiúscula após o prefixo (por exemplo, lua_Number).

Com o uso de funções da API é possível interagir com a pilha virtual, seja inserindo ou removendo valores, ou realizando as diversas operações que podem ser executadas em Lua, como atribuição de valores ou execução de funções, por exemplo, tendo os itens da pilha como parâmetros, e podendo também deixar o retorno das funções executadas na própria pilha.

Além das funções da biblioteca Lua, propriamente, existe também uma biblioteca auxiliar, a qual também faz parte da distribuição padrão, e que oferece um pouco mais de ergonomia na manipulação da API básica.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Biblioteca auxiliar

Embora a API C forneça todas as operações necessárias para interagir com um estado Lua, as funções são relativamente cruas ou em alguns casos tediosas de utilizar devido ao fato de exigir alguns parâmetros cujo valor poderia ser o mesmo para a maior parte dos casos.

No sentido de oferecer um pouco mais de ergonomia para a API, a biblioteca auxiliar fornece funções que estendem a API, facilitando operações que são comuns. Essa biblioteca também faz parte da distribuição padrão. Suas funções e tipos são declarados no arquivo de cabeçalho lauxlib.h.

Para diferenciar das funções e tipos da API principal, as funções e tipos tem a convenção de iniciar pelo prefixo luaL_ no lugar de lua_. A convenção de iniciar os tipos (após o prefixo) com uma maiúscula, no entanto, também é utilizada do na biblioteca auxiliar, sem diferença com a API principal.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Biblioteca padrão

A biblioteca padrão é uma coleção de variáveis disponíveis no ambiente global, a maioria das quais são agrupadas em módulos, voltados para um tipo de operação.

Por exemplo, o módulo math dispõe várias funções matemáticas como seno e logaritmo, e também constantes (não no sentido de constantes Lua, mas sim no sentido matemático mesmo) como PI, enquanto o módulo io dispõe funções para interação com a entrada e a saída do programa (o que permite ler e escrever em arquivos).

Todas as funções da biblioteca padrão são implementadas em C utilizando a API C de Lua, e podem ou não estar disponíveis para um programa Lua, a depender de como o programa hospedeiro foi compilado. O interpretador Lua, por exemplo, é compilado de modo a disponibilizar a biblioteca padrão para o código Lua.

Ao longo das versões de Lua, a biblioteca padrão eventualmente sofre algumas mudanças, incluindo o retirando alguns módulos (como o utf8 que passou a existir a partir da versão 5.3, ao mesmo tempo que o bit32 foi descontinuado), ou também incluindo ou retirando funções dos módulos existentes.

No interpretador Lua os módulos da biblioteca padrão não precisam ser importados, eles já estão disponíveis no ambiente global, porém outros programas hospedeiros podem eventualmente optar por não incluí-los.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Bloco

Um bloco é uma sequência de zero ou mais comandos, e o início de um bloco é sempre o início de um escopo, dentro do qual variáveis locais podem ser criadas e persistidas até o fim do bloco.

Blocos podem ser criados de várias maneiras. O corpo de uma função é um bloco, assim como o corpo de um laço iterativo ou o de uma estrutura condicional. Estas são formas implícitas de criação de um bloco. Blocos também podem ser criados explicitamente com o uso da estrutura do ... end.

Exemplo:

function a()
    -- bloco como corpo da função
    if b == 1 then
        -- bloco como corpo da condição verdadeira
    else
        -- bloco como corpo da condição falsa
    end
end

do
    -- bloco explícito
end

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Chamada final

Uma chamada final é uma chamada de função realizada como retorno de outra função, sem que haja qualquer ação posterior dentro da função chamadora. Deste modo, o retorno da função chamada é também o retorno da função chamadora.

Lua implementa chamadas finais próprias, de modo que a pilha de chamadas nunca aumenta em uma sequência de chamadas, se todas as chamadas forem finais. Isto tem uma grande utilidade em funções recursivas (diretas ou indiretas).

Exemplo:

function a()
    -- o retorno de b() é também o retorno de a()
    return b()
end

Com o surgimento de variáveis fecháveis na versão 5.4, o conceito de chamadas finais expandiu um pouco, ao considerar que uma chamada final não pode estar no escopo de uma variável fechável.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Chamada protegida

Como em outras linguagens de programação, por diferentes motivos, erros podem ocorrer durante a execução do programa. Situações que culminam em um erro variam, podendo se dever a uma operação com o tipo errado, por exemplo, ou quando uma condição da função assert() não é atendida (o que geralmente serve para prevenir erros lógicos).

Um erro interrompe a execução do programa, o que pode ou não ser desejável. Quando não é (ou seja, quando o programa deve continuar a executar mesmo quando ocorrerem erros), um tratamento deverá ser feito para o erro, capturando o erro em uma chamada protegida.

Uma chamada protegida é uma chamada de função na qual o corpo da função é executado até o final ou é interrompida no primeiro erro que ocorrer, e caso haja um erro, ele não é propagado de modo a interromper o programa. Em vez disso, essa chamada retorna false denotando que não houve sucesso na execução, e uma mensagem de erro (que pode ser de qualquer tipo), que determina o ponto em que houve erro e o motivo.

Exemplo:

local function recnum(n)
    return assert( tonumber(n),
        "O valor recebido (" 
            .. tostring(n)
            .. ") não é um número.")
end

local ok, err = pcall( function()
    total = recnum(42)              -- até aqui, ok
    total = total + recnum("abc")   -- ERRO (capturado)
end)

if ok then
    print("Valor retornado: " .. tostring(total))
else
    print("Ocorreu um erro:\n  " .. err)
end

Neste exemplo, a função pcall() é utilizada para estabelecer um contexto protegido para a função anônima que recebe, dentro da qual fatalmente um erro ocorrerá. A função xpcall() poderia ser usada para o mesmo fim, embora essa seja um pouco mais sofisticada pois recebe como parâmetro uma função para operar sobre a mensagem de erro. Para além disso, também é possível executar chamadas protegidas usando a função lua_pcall() da API C.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Co-rotina

Lua implementa um mecanismo de controle (co-rotina) que permite criar distintos fluxos de execução independentes, cada qual dentro da própria pilha de chamadas, e esses fluxos são colaborativos, no sentido de que transferem o controle da execução uns para os outros, possivelmente passando e/ou retornando parâmetros.

A transferência de controle é feita em duas vias, isto é, uma co-rotina pode ser chamada por uma função que transfere o controle do programa para ela, e pode ser suspensa por outra função, que devolve o controle para o fluxo no qual foi chamada. Cabe notar que apenas uma co-rotina pode estar em execução em um dado momento, isto é, não há o conceito de paralelismo entre co-rotinas.

Ao residir em uma pilha de chamadas diferente, cada co-rotina mantem seu próprio estado, e este estado é suspenso ou retomado junto com a co-rotina, como se congelasse durante o período de suspensão. Co-rotinas são representadas pelo tipo thread, que representa um fluxo de execução independente.

Exemplo:

co = coroutine.create( function(x)
    -- devolver o controle para o contexto
    -- em que foi chamada:
    y = coroutine.yield(x+1)
    -- retornar (e encerrar essa co-rotina)
    return y * 2
end)

-- Chamar a co-rotina co passando um parâmetro numérico
a = coroutine.resume(co, 2) -- x = 2, a = 3
b = coroutine.resume(co, 8) -- y = 8, b = 16

Cabe dizer também que funções chamadas dentro da co-rotina também podem devolver o controle para o fluxo anterior, e quando essa co-rotina for acionada novamente, retomará a execução a partir da função interna que havia feito essa devolução.

Co-rotinas são úteis para implementar geradores e varrer algumas estruturas de dados, como árvores binárias, por exemplo, usando código enxuto e elegante.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Coletor de lixo

A coleta de lixo é um termo usado para tratar sobre gerenciamento de memória. À medida que a aplicação aloca valores em variáveis, esses valores deverão constar em algum lugar na memória. Quando esses valores não são mais necessários, eles precisam ser descartados, para que a memória utilizada seja liberada para outros usos.

Há diferentes maneiras de lidar com essa liberação, e Lua utiliza um mecanismo também bastante empregado em outras linguagens, que é conhecido como coletor de lixo. Trata-se de uma rotina que automaticamente detecta valores não mais necessários e libera a memória utilizada por eles.

Valores são dados como descartáveis quando inacessíveis em Lua. Por exemplo:

t = {} -- tabela criada
t = nil
-- agora a tabela criada está inacessível e
-- será marcada para coleta

Cinco tipos de valores estão sujeitos a coleta de lixo: cadeias de caracteres (strings), tabelas, funções, fluxos de execução (threads) e userdata completo. Ou seja, valores desses tipos podem ser coletados quando não mais acessíveis.

O coletor trabalha em modo incremental por padrão. Na versão 5.2, um novo modo foi criado de forma experimental, o modo geracional, que parte da premissa de que a maior parte dos objetos criados morre jovem, isto é, permanece dentro de alcance por pouco tempo. Esse modo é útil para aplicações que criam e descartam rapidamente muitos valores.

Na versão 5.3, o modo geracional (que ainda era experimental) foi removido, porém na versão 5.4 ele foi disponibilizado novamente, já não mais como experimento.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Compilação Lua

Falar em compilação em Lua pode causar alguma confusão. Afinal, ao menos na distribuição padrão (mas também em várias outras implementações) sabe-se que o código Lua é interpretado. O que ocorre é que o termo compilação assume um significado um pouco diferente neste caso.

Ao falar em compilar trechos de código Lua, fala-se em converter o código Lua para o formato bytecode, um formato intermediário, binário, que será lido pela máquina virtual Lua. Não se trata, portanto, de um processo que ao final gera código de máquina, como seria em C, por exemplo.

Tanto o programa de linha de comando luac quanto a função Lua string.dump() realizam a conversão de código Lua (portanto em formato textual) para bytecode (binário). A vantagem em pré-compilar o código Lua resume-se a três fatores:

  1. Carregar código pré-compilado ganha algum tempo (embora o tempo de execução não tenha ganhos em função disso);
  2. Proteger o código de alterações acidentais;
  3. Identificar possíveis erros de sintaxe no código, evitando que eles sejam disparados durante a execução do programa.

Exemplo:

local bc = string.dump ( function() print("olá") end )
-- bc agora guarda código Lua em bytecode
-- (uma função que apenas exibe "olá")

local f = load(bc) -- a função load carrega o código
                   -- bytecode novamente, e devolve
                   -- uma função, guardada em f
f() --> olá

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Constante

Uma constante é simplesmente uma variável que não pode ser alterada após sua inicialização, até que saia de escopo. Tenha em mente que não inicializar uma constante significa, na prática, inicializá-la com nil, e não será possível atribuir nenhum valor posteriormente.

Exemplo:

local a <const> = 3 -- a é uma constante
-- a = 4 --> ERRO (a não pode mudar)

Este conceito está presente nas versões: 5.4

Depuração

Programar sempre envolve o risco de criar programas que não funcionam ou apresentam comportamento inesperado. Quando isso acontece, o programa deve ser inspecionado para que se identifique quais partes dele são responsáveis pelos erros apresentados. Em muitos ambientes de desenvolvimento é comum o uso de ferramentas conhecidas como depuradores.

Depuradores são úteis para inspecionar as entranhas do programa enquanto ele é executado, observando o que ocorre internamente (quais variáveis são declaradas, em que momento, e com quais valores, quais funções estão sendo executadas, etc.), semelhante a uma biópsia, na qual o organismo examinado está vivo.

Lua não oferece um depurador, propriamente, mas possui uma interface de depuração composta por diversas funções e tipos que permitem construir depuradores personalizados para cada aplicação. Essa interface tanto é acessível pela API C quanto pelo módulo debug da biblioteca padrão, o qual por sua vez utiliza a API C.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Distribuição padrão

Lua é software livre (licença MIT), e como tal ele pode ser modificado e redistribuído, respeitada a sua licença. Devido ao sucesso que fez como linguagem embarcada em diversas plataformas, várias versões modificadas foram criadas e redistribuídas.

Porém há uma versão “oficial”, a distribuição feita pela PUC Rio, cujas versões estão disponíveis neste endereço. Os pacotes distribuídos incluem a documentação e todo o código-fonte necessário para compilar a biblioteca Lua, toda a sua biblioteca padrão e o interpretador Lua.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Ephemeron

O conceito de ephemeron está relacionado a ambientes de desenvolvimento com coleta de lixo, como Lua, nos quais é possível definir pares de chave-valor com uma referência fraca na chave (o que em Lua é possível com o uso de tabelas fracas).

Um ephemeron é uma estrutura de dados útil em sistemas desse tipo. Ele é caracterizado por uma relação entre chaves e valores na qual a chave pode ser marcada para coleta de lixo, ainda que o valor correspondente a essa chave faça alguma referência a ela.

Cabe lembrar, se houver referências fortes a um objeto, ele não pode ser recolhido. Nesse sentido, a referência de um valor para a chave correspondente é tratado como uma referência fraca em Lua. E isso faz com que tabelas fracas em Lua, cuja fraqueza da relação é dada pela chave sejam, para todos os efeitos, um ephemeron.

Um ephemeron pode ser usado para associar objetos a um grupo de propriedades, de modo que quando o objeto é recolhido, suas propriedades também são. Também é útil para trabalhar o conceito de atributos privados no paradigma de orientação a objetos, por exemplo.

Este conceito está presente nas versões: 5.4, 5.3, 5.2,

Escopo léxico

Esta é uma das características mais importantes da linguagem Lua. O termo escopo léxico refere-se ao fato de que variáveis locais são acessíveis ao longo de todo o bloco onde são declaradas (a partir do ponto em que foram declaradas), e isto inclui blocos internos, como o corpo de funções.

Deste modo, é possível acessar a partir do corpo de uma função uma variável definida fora dessa função, desde que seja uma variável local externa. Isto fará com que essa variável seja lida pela função, mesmo quando a função for chamada em um contexto diferente, no qual a referida variável não esteja acessível.

O escopo léxico, associado a uma outra característica importante, os valores de primeira classe, também presente em Lua, é o que permite que se construam fechos nesta linguagem.

É importante notar também que essas regras dizem respeito à ocultação de variáveis, quando duas variáveis distintas possuem o mesmo nome, o local de onde forem acessadas interferirá no acesso, pois será priorizada aquela que estiver no bloco mais interno.

Exemplo:

local x = 1
do -- novo bloco mais interno

    local x = 2 -- nova variável local, oculta a anterior
    print(x) --> 2

end -- a variável x interna deixa de existir a partir daqui
print(x) --> 1

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Estado Lua

Podemos imaginar um estado Lua como uma área virtual opaca (isto é, à qual não temos acesso diretamente), na qual residirão todas as variáveis Lua. Somente é possível acessar essas variáveis por meio da API C, com o auxílio de uma pilha virtual.

Um estado Lua é composto por um ou mais fluxos de execução (por padrão, apenas um, embora novos fluxos possam ser criados para um estado posteriormente ao primeiro).

Um programa hospedeiro pode trabalhar com mais de um estado, e neste caso cada estado terá sua própria pilha virtual para realizar a interface com o hospedeiro.

Os estados são representados em C pelo tipo lua_State. Embora esse tipo seja na realidade um ponteiro para um fluxo de execução (no caso, o fluxo em atividade para um estado), na prática ele faz referência a todo o estado do qual o fluxo faz parte.

O trecho de código C a seguir mostra a criação e encerramento de um estado Lua:

/* Criar novo estado Lua */
lua_State *L = luaL_newstate();

/* realizar operações com o estado criado */

/* Encerrar estado
   (ficará inacessível e memória será liberada) */
lua_close(L);

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Fecho

Lua possui uma interessante combinação: escopo léxico e valores de primeira classe. Isto abre espaço para uma estrutura conhecida como fecho (também usualmente chamada de clausura). Trata-se de uma combinação entre funções e valores guardados em variáveis locais externas, de modo que após sair de escopo essas variáveis só podem ser acessadas pela função e não mais diretamente.

Fechos são sempre funções retornadas por outras funções, possuindo dentro de sua lógica acessos (seja de leitura ou de escrita) a variáveis externas locais existentes no contexto da função mais externa, isto é, a função que cria o fecho. Vejamos um exemplo:

local function cria_contador(x)
    -- como x é um parâmetro formal, ele é também uma
    -- variável local
    x = x or 0
    return function()
        -- para esta função interna, x é uma variável
        -- local externa (upvalue)
        x = x + 1
        return x
    end
end

c1 = cria_contador()
c2 = cria_contador(10)

print(c1()) --> 1
print(c1()) --> 2
print(c1()) --> 3

print(c2()) --> 11
print(c2()) --> 12

Note no exemplo acima que dois contadores independentes são criados e cada um possui seu próprio valor para x, isto é, cada um mantém seu próprio estado. Note também que esse valor é incrementado a cada chamada, portanto o estado é persistido entre chamadas.

E além disso, outra observação importante é que o que a função mais externa (cria_contador()) retorna não é o retorno da função mais interna, mas sim a própria definição da função, ou seja, a função propriamente. Isto é o que permite que a função interna seja chamada posteriormente, tantas vezes quanto se julgar apropriado.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Finalizador

Um finalizador é um metamétodo que é chamado quando o objeto associado é marcado para coleta de lixo. Isso é útil para fechar arquivos, conexões de rede ou outros recursos utilizados por ele. No caso de valores userdata isto pode ser usado inclusive para realizar a pŕopria liberação da memória utilizada.

Para criar um finalizador, deve-se criar uma metatabela com o campo __gc, que guardará o finalizador, e então associar essa metatabela ao valor que estará sujeito à coleta. Por exemplo:

t = {
    arquivo = io.tmpfile() -- arquivo aberto
}
mt = {
    __gc = function(a)
        -- antes da coleta, fechar o arquivo
        a.arquivo:close()
    end
}
setmetatable(t,mt)

No exemplo acima, a tabela t possui um campo arquivo que aponta para um arquivo temporário aberto. Para evitar que o arquivo permaneça aberto por mais tempo que o necessário, essa tabela é vinculada a uma metatabela que implementa o metamétodo __gc(), no qual o arquivo apontado pelo campo arquivo será fechado antes da coleta.

Cabe frisar que na versão 5.1, o conceito de finalizador só se aplica a valores do tipo userdata. Somente a partir da versão 5.2 ele também pode ser usado em tabelas.

Além disso, a partir da versão 5.4 existe uma nova forma de implementar finalizadores, que é fazendo uso de variáveis fecháveis, e seu metamétodo __close. Isto aumenta o controle sobre o processo de finalização, pois esse metamétodo é chamado no exato momento em que a variável sai de escopo.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Fluxo de execução

Um fluxo de execução em Lua é denotado pelo tipo thread. O nome thread pode causar alguma confusão com o conceito de threads do sistema operacional. Não se trata disso. O tipo thread em Lua é usado para implementar as co-rotinas, e a sua suspensão é controlada explicitamente com uma chamada de função realizada de dentro da co-rotina.

A criação e manipulação de valores do tipo thread é realizada por meio de funções da biblioteca padrão (no módulo coroutine). Por exemplo:

co = coroutine.create(function(n)
    while true do
        coroutine.yield(n)
        n = n + 1
    end
end)

print(co) --> thread: 0x558c08cab0e8
          --  (ou qualquer outra identificação de
          --  fluxo, varia com a execução)

coroutine.status(co) --> suspended
coroutine.close(co)
coroutine.status(co) --> dead

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Função anônima

Em muitas linguagens de programação, o uso de funções depende de um nome atribuído à função. Nessas linguagens, funções são valores de segunda classe, ou seja, não podem ser guardados em variáveis, passados como argumentos de outras funções, ou retornados de outras funções.

Enquanto valores de segunda classe, as funções só podem ser acessadas pelo seu nome. Em Lua, no entanto, funções são valores de primeira classe, e por isso podem ser acessadas de outros modos.

Por exemplo:

function f()
    return function()
        print "Olá"
    end
end

-- acessando diretamente o retorno de f()
(f())()   --> Olá

-- salvando em uma variável auxiliar
g = f()
g()       --> Olá

Veja que a função interna sequer possui um nome. Mas ela é retornada pela função f, e pode ser chamada, tanto diretamente como resultado da chamada a f, ou sendo atribuída primeiro a uma outra variável auxiliar (neste caso, g) para então ser chamada.

Isto pode levar a crer que a primeira função, f possui um nome, e a segunda (atribuída a g) não possui. Mas não. Na realidade, o nome f neste caso é apenas uma variável chamada f que guarda uma função.

Portanto, na prática toda função em Lua é anônima (ao menos enquanto considerarmos que um nome de função é um símbolo indissociável dessa função). Por meio de açúcar sintático definimos funções como se elas tivessem um nome, porém na prática as duas definições a seguir possuem o mesmo efeito:

-- com açúcar sintático
function f(t) return t end

-- sem açúcar sintático
f = function(t) return t end

Nos dois casos acima, f possui o mesmo valor (uma função que retorna seu primeiro argumento).

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Interpretador Lua

A distribuição padrão de Lua inclui um programa chamado lua. Trata-se de um programa hospedeiro escrito em C, que usa a API C de Lua para executar código Lua, tanto interativamente quanto em lote (comandos lidos de um ou mais arquivos-fonte Lua).

Este programa é usualmente referido como o interpretador Lua, ou apenas lua (com a inicial em minúscula mesmo). Ele é muito usado para testar e depurar programas, mas também para executar programas Lua auto-suficientes, ou seja, programas cuja lógica principal está implementada em código Lua.

Por padrão, o interpretador disponibiliza toda biblioteca padrão, tanto para uso interativo, como para execução de programas em lote.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Máquina virtual

Na distribuição padrão, a biblioteca Lua interpreta o código Lua, num processo de duas etapas. Primeiro o código em formato texto é convertido para bytecode, um formato binário que contempla instruções básicas para uma máquina virtual. Esse primeiro passo é chamado de compilação na terminologia de Lua.

Feito isso, as instruções presentes no código em bytecode são executadas pela máquina virtual, o que envolve traduzir essas operações para código de máquina e então efetivamente executar as instruções geradas.

Dado que o termo “máquina virtual” pode assumir significados diferentes, cabe aqui destacar que não se trata de uma emulação de um sistema operacional ou mesmo de uma camada de conteinerização, e sim de uma máquina virtual de processo (no caso, do programa hospedeiro), que é criada pelo processo e encerrada junto com ele.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Metamétodo

Um metamétodo é um campo de uma metatabela que possui como propriedade a capacidade de ser chamado (como uma função). Isso se aplica não apenas a funções (que podem ser campos de uma tabela) mas também a qualquer valor que tenha sido alterado usando metamétodos para poder ser chamado como se fosse uma função.

Parece confuso? Vejamos um exemplo:

mt = {
    -- metamétodo __call determina o que fazer se
    -- o valor for chamado como uma função.
    __call = function(c) print(c) end

    -- Neste caso, apenas exibe o valor chamado (c)
    -- na tela, usando print.
}
t = {}
setmetatable(t,mt) -- t possui a metatabela mt
t() --> table: 0x55ac4262aca0
    --  (ou qualquer outra identificação de
    --  tabela, varia com a execução)

Neste exemplo, usamos o metamétodo __call que será chamado toda vez que o valor vinculado à metatabela mt (portanto t neste caso) for chamado como se fosse uma função. Embora não seja função, pode ser chamado pois possui este metamétodo.

Existem outros metamétodos especiais, que podem ser usados para alterar o comportamento de um valor, permitindo que ele seja, por exemplo, usado em operações aritméticas sem ser um número, concatenado sem ser textual ou implementar associações sem ser uma tabela, por exemplo.

Perceba, portanto que um metamétodo não precisa ser uma função, pois pode ser qualquer valor associado a uma metatabela com um metamétodo __call.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Metatabela

Todo valor em Lua pode ter alguns metadados associados, armazenados em uma tabela, convencionalmente chamada de metatabela. Valores diferentes (inclusive de tipos diferentes, embora não seja comum) podem compartilhar uma mesma metatabela.

Por outro lado, para um valor qualquer que seja, só é possível associar uma única metatabela, isto é, não é possível associar um valor a duas ou mais metatabelas. Além disso, em código Lua só é possível configurar metatabelas para tabelas. Para os demais tipos, só é possível fazer associação com uma metatabela via API C ou usando a biblioteca de depuração.

Metatabelas podem ser usadas para reconfigurar o comportamento do valor associado, como definir o que será feito quando ele for usado em uma operação de adição, chamada ou conversão para texto, apenas para citar alguns. Isto é feito com o uso de metavalores (valores da metatabela). Metavalores que podem ser chamados (no sentido de chamada de função) são conhecidos como metamétodos.

Para além disso, em tabelas e valores do tipo userdata, metatabelas são úteis para diferenciar esses valores como um tipo específico.

Exemplo:

local cor = {} -- metatabela de cores
local azul = { vermelho = 0, verde = 0, azul = 255 }

setmetatable(azul, cor) -- associação feita
                        -- (tabela azul possui metatabela cor)
-- ...

-- verificar se azul possui metatabela cor
if getmetatable(azul) == cor then
    -- realiza operações com a variável azul, que são
    -- específicas para tabelas do tipo "cor"
end

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Módulo

Não é incomum que programadores se preocupem em reutilizar código, isto é, reaproveitar trabalhos feitos anteriormente (inclusive por terceiros) para evitar ter que reescrever o mesmo código a cada projeto. Para isto, é preciso organizar o código em módulos que possam ser importados para cada projeto.

Lua oferece suporte a módulos, baseado em um mecanismo bastante simples, porém efetivo. Cada módulo é representado por uma tabela, que poderá ter submódulos (outras tabelas). Como uma tabela já é por si só um espaço de nomes, os nomes utilizados em um módulo não entram em conflito com nomes utilizados em outros módulos.

Módulos podem ser implementados em Lua ou em outras linguagens e disponibilizados via API C. Lua suporta ligação dinâmica em sistemas nos quais esse recurso é suportado, portanto é possível criar módulos e distribuí-los como bibliotecas compartilhadas.

Módulos Lua tipicamente são escritos em um arquivo-fonte separado (ou conjunto de arquivos, quando há submódulos). Como um arquivo-fonte representa um trecho, ele é tratado como uma função, e portanto um comando de retorno ao final deve retornar a tabela representativa do módulo.

A biblioteca padrão possui a função require(), que é usada para importar módulos em um programa Lua. Ela realiza a pesquisa em um conjunto de diretórios pré-definidos (o que pode ser configurado) por arquivos que correspondam ao nome do módulo, e devolve uma tabela retornada pelo arquivo.

Cabe destacar uma incompatibilidade entre versões nesse quesito: a versão 5.1 adicionou a função module() à biblioteca padrão como recurso para criação de novos módulos. Porém se constatou posteriormente que esse recurso era supérfluo para a linguagem, e na versão 5.2 ele caiu em depreciação, sendo depois removido na versão 5.3.

É possível criar módulos compatíveis com todas as versões recentes, ao evitar o uso da função module(). Por exemplo:

-- arquivo-fonte imc.lua

local _M = {
    imc = function(peso, altura)
        return peso / altura ^ 2
    end
}

return _M -- módulo retornado

O exemplo acima demonstra um módulo simplório que possui apenas uma função. A função require() pode ser usada para importá-lo e utilizar a função.

local imc = require "imc" -- módulo importado e guardado
                          -- na tabela local "imc"

print(imc.imc(68, 1.72)) --> 22.985397512169

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Pilha virtual

Uma pilha virtual (no contexto de Lua) é uma estrutura que permite o intercâmbio de valores entre o programa hospedeiro e um estado Lua. Ela é criada junto com o estado correspondente e ajuda a contornar dois problemas básicos:

  1. C e Lua possuem sistemas de tipos diferentes, portanto é preciso estabelecer um mecanismo pelo qual os tipos Lua possam ser referidos em código C, e vice-versa;
  2. C e Lua possuem estratégias diferentes para o gerenciamento de memória: enquanto em C o gerenciamento é manual, em Lua, é automático, por meio do coletor de lixo.

Como qualquer estrutura de pilha, guarda novos valores no topo, e também remove valores a partir do topo. Essa regra é seguida à risca pelo código Lua, entretanto o programa hospedeiro também pode realizar acessos a partir da base da pilha.

Cada elemento da pilha possui um índice que o identifica. Índices positivos começam a partir da base, ou seja, o índice 1 corresponde ao item mais ao fundo da pilha, o índice 2 corresponde ao item logo acima deste, e assim por diante. Índices negativos fazem o acesso reverso, isto é, a partir do topo, de modo que o item -1 será o item mais ao topo da pilha, -2 logo abaixo deste, e assim por diante.

O trecho de código C a seguir mostra alguns exemplos de inserção e remoção de valores em uma pilha virtual usando a API C:

/* Criar novo estado (e sua pilha virtual junto) */
lua_State *L = luaL_newstate();

/* Inserir inteiro (42) na pilha */
lua_pushinteger(L, 42);
/* Tamanho da pilha: 1,
   L[1] = L[-1] = 42 */

/* Inserir booleano (true) na pilha */
lua_pushboolean(L, 1);
/* Tamanho da pilha: 2,
   L[2] = L[-1] = true
   L[1] = L[-2] = 42 */

/* Remover o item mais ao topo (true) */
lua_remove(L, -1);
/* Tamanho da pilha: 1,
   L[1] = L[-1] = 42 */

lua_close(L);

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Programa hospedeiro

Em Lua, não há o conceito de função principal, como há em C. Em vez disso, todo o código Lua é chamado por um programa hospedeiro, escrito em outra linguagem (geralmente C ou C++), tendo em vista que Lua é uma biblioteca usada por esse programa hospedeiro.

Por meio da API C, o programa hospedeiro interage com o código Lua, seja lendo variáveis definidas no contexto Lua, modificando o valor dessas variáveis, chamando funções Lua, registrando funções C que podem ser chamadas em Lua ou criando novos tipos userdata.

Deste modo, o programa Lua estará sempre subordinado ao programa hospedeiro, ainda que a funcionalidade principal do programa esteja implementada no código Lua. Muitas das vezes, o programa hospedeiro é o próprio interpretador Lua.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Tabela

Uma tabela é um tipo de valor altamente versátil, que implementa vetores associativos, nos quais o índice pode ser não apenas numérico, mas também de outros tipos, como cadeias de caracteres, booleanos, e até mesmo funções, assim como podem também armazenar valores de diferentes tipos. Isso permite que tabelas sejam usadas para implementar diversas estruturas de dados.

Exemplo:

t      = {} -- construção da tabela, guardada em t
t[1]   = 1  -- indexação de valor na tabela
t[2]   = 2 
t["a"] = 10 -- índice não numérico (string)

Existe um açúcar sintático para o acesso a índices textuais em tabelas.

Exemplo:

t = { a = 10 } -- tabela criada com o campo "a" = 10
print(t["a"] == t.a) --> true

Tabelas são consideradas objetos, portanto são estruturas mutáveis e são sempre acessadas por referência, isto é, nunca são duplicadas quando uma nova variável aponta para elas, apenas referenciadas. Quando não há mais nenhuma referência a uma tabela, ela será recolhida em algum momento pelo coletor de lixo.

Para além disso, tabelas podem ter seu comportamento estendido por meio de metatabelas e metamétodos. Isto permite, entre outras coisas, que tabelas sejam usadas para implementar o paradigma de orientação a objetos.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Tabela fraca

Uma tabela fraca é uma tabela na qual seus elementos serão marcados para coleta de lixo se eles só forem referenciados por esta tabela. Existem 3 modalidades de tabela fraca:

Para qualquer um dos modos, se houver a coleta da chave ou do valor, o par será removido da tabela, pois não haverá mais associação a ser feita. Vale lembrar que nem todos os tipos em Lua são sujeitos à coleta de lixo. Portanto valores como números e booleanos, por exemplo, permanecem intactos em uma tabela fraca.

Uma tabela se torna uma tabela fraca a partir do momento em que lhe é atribuída uma metatabela com o campo __mode, cujo valor deverá ser "k" para chaves fracas, "v" para valores fracos ou "kv" para que tanto as chaves quanto os valores sejam fracos.

Exemplo:

local cores = {}
setmetatable(cores, { __mode = "v" })

local azul = { r = 0, g = 0, b = 255 }
cores.azul = azul
azul = nil -- cores.azul será marcado para coleta
           -- pois a referência externa a essa tabela
           -- não existe mais.

Tabelas fracas podem ser usadas para implementar um ephemeron, ao utilizar chaves fracas (isto é, para __mode = "k").

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Trecho

Sintaticamente, um trecho (chunk, na nomenclatura original, em inglês), é idêntico a um bloco, ou seja, ele é formado por uma sequência de comandos. Do ponto de vista do interpretador lua, um trecho é uma função anônima, que pode receber parâmetros e também retornar valores.

Um trecho é uma unidade de compilação, isto é, os comandos que fazem parte de um trecho são compilados em conjunto para bytecode (instruções para a máquina virtual Lua, em formato binário). Ele pode ser armazenado na forma de texto ou bytecode em arquivos ou em variáveis do programa hospedeiro.

Há uma diferença significativa entre trechos na versão 5.1 e nas demais versões, que diz respeito ao conceito de ambientes, o qual mudou a partir da versão 5.2. A partir dessa versão, todo trecho possui como variável local externa a tabela _ENV.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Userdata

O tipo userdata é responsável por grande parte da extensibilidade da linguagem Lua, pois ele pode ser usado para definir novos tipos personalizados usando a API C. Esse tipo não pode especificado ou alterado em código Lua, apenas no programa hospedeiro. Em Lua, cabe apenas criar ou comparar valores desse tipo (as duas únicas operações disponíveis).

Em essência, um userdata nada mais é que um bloco de memória. Ele pode ou não ser gerenciado automaticamente, e portanto há uma distinção entre duas formas de userdata:

Valores userdata completos podem possuir metatabelas, porém ao contrário do que ocorre com tabelas, a associação de um userdata com uma metatabela só pode ser feita no programa hospedeiro, ou seja, não é possível associar uma metatabela a um userdata usando a função setmetatable() em Lua.

O uso de metatabelas neste caso permite que mais operações (além de criar e comparar) sejam especificadas para um determinado tipo userdata completo, bem como permite que finalizadores também sejam especificados para o tipo em questão.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Valores de primeira classe

Em Lua, todos os oito tipos de dados podem ser armazenados em variáveis, passados como argumentos para uma função ou retornados como resultado de uma função. Neste caso é dito que eles são valores de primeira classe.

Dentre os oito tipos, talvez chame mais atenção o fato de as funções serem também tratadas desta maneira. Isto abre possibilidades interessantes para a linguagem, como os metamétodos e a construção de fechos (tendo em vista que a linguagem também possui regras de escopo léxico.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Variável fechável

Uma variável fechável funciona como uma constante, ou seja, uma vez declarada, ela não pode ter seu valor alterado até sair de escopo. A diferença em relação a constantes normais é que esse tipo de constante deve possuir um metamétodo __close, que será chamado quando essa variável sair de escopo.

Exemplo:

local t = {}
local mt = {
    __close = function() print "Fechado" end,
}
setmetatable(t, mt)

local a <close> = t -- a é uma constante fechável

-- a = {} --> ERRO (a não pode mudar)

-- Ao encerrar este bloco, t sai de escopo e seu
-- metamétodo __close() é chamado, exibindo a
-- mensagem "Fechado".

Este conceito está presente nas versões: 5.4

Variável global

Em Lua, uma variável global é um campo da tabela que representa o ambiente vigente, ou seja, da tabela _ENV (a partir da versão 5.2) ou da tabela retornada por getfenv() até a versão 5.1.

Toda referência a uma variável global se traduz para o acesso à tabela do ambiente atual (o que significa que na prática toda variável global é um campo de uma tabela).

Exemplo:

varg = 1 -- variável global criada
print(varg == _ENV.varg) --> true

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Variável local

Variáveis locais são aquelas que seguem as regras de escopo léxico, e portanto são acessíveis apenas dentro do bloco em que foram criadas e blocos internos a este. Uma variável, ao ser declarada, deverá ser explicitamente marcada como local, do contrário será global.

Exemplo:

x = 10          -- x é uma variável global
local y = 11    -- y é uma variável local

É boa prática declarar as variáveis como locais, sempre que possível, pois ao fazer muito uso de variáveis globais, elas poderão entrar em conflito de nomes ao longo do programa e interferir no resultado de modo inesperado.

Vale lembrar que uma variável local não precisa ser inicializada durante a sua declaração, ou seja, ela pode ser apenas declarada e receber algum valor em um local posterior do código. Por exemplo:

local y
-- outras coisas acontecem no programa

y = 11 -- y neste caso é local, pois já foi declarada

Outro aspecto importante a se considerar sobre variáveis locais é que parâmetros de função também são uma forma de variável local.

function ola(nome)
    -- nome é uma variável local, que pertence ao
    -- bloco composto pelo corpo da função
    print("Olá, " .. nome)
end

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1

Variável local externa

Também chamadas de upvalues (valores “de cima”), as variáveis locais externas são assim chamadas por não serem globais, porém do ponto de vista de uma função, elas são externas, ou seja, não são definidas dentro da função.

Ou seja, trata-se de uma variável local criada fora da função, porém visível dentro dela, devido a regras de escopo léxico.

Exemplo:

local i = 10

function minha_funcao()
    -- i é visível aqui
    -- portanto i é um upvalue de minha_funcao()
end

Também é possível criar funções que acessam variáveis locais externas em C, usando a API C de Lua.

Este conceito está presente nas versões: 5.4, 5.3, 5.2, 5.1