Closures: um escopo local persistente
Cidadãos de primeira classe e escopo léxico
Hoje vou falar sobre uma interessante técnica chamada closures! Vamos aprender o que são closures e como implementá-las! Para tal, utilizarei a linguagem Python que é uma linguagem de fácil leitura e será bastante útil para aprendermos este conceito que, a princípio, pode ser um pouco complicado! Ah! E já adianto: closures tem várias aplicações em Python! Já usou decoradores de funções? Se sim, já pode ter usado closures e nem sabia! Vamos aprender isso de uma vez?
Antes de aprendermos o conceito de closures, precisamos de um pouquinho de teoria para que tudo faça sentido! Precisamos de 2 conceitos definidos no design de linguagens de programação: cidadãos de primeira classe (calma, não tem nada haver com dinheiro!) e escopo léxico.
Cidadãos de primeira classe:
Em Python, funções são cidadãos de primeira classe. Vejamos a definição da Wikipedia sobre este conceito:
In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable.
Dessa forma, observamos que, basicamente, funções em Python se comportam como qualquer outro objeto na linguagem (algo esperado, afinal, tudo em Python é um objeto!). Assim, podemos declarar funções em diversos lugares do código como fazemos com variáveis e, inclusive, podemos declarar funções dentro de outras! Podemos, também, passá-las como argumentos para outras funções bem como retorná-las!
Assim, podemos pensar em funções quase como valores pois podemos passar como parâmetro, declarar, retornar…
Um detalhe importante: funções que:
- recebem funções como argumentos; ou
- retornam outras funções
São denominadas como funções de ordem superior. Funções que não suprem tais condições são denominadas funções de primeira ordem.
PS: Uma curiosidade: este é um dos grandes “pilares” de programação funcional!
Escopo Léxico
De forma simples, podemos definir escopo como a região no programa onde um determinado binding existe e pode ser referenciado. Quando criamos uma variável em Python:
x = 2
Criamos um binding (ligação) entre o identificador x e a entidade 2 (objeto tipo int). Assim, nesta região do programa, podemos utilizar o identificador x para referenciar seu objeto (o inteiro 2). Contudo, em outras regiões do programa que definem seus próprios escopos, o mesmo identificador x pode referenciar outro objeto ou até mesmo não referenciar nada. Assim, escopo pode ser pensado como uma região onde um certo binding é visível.
Python, como muitas outras linguagens, é implementada para utilizar escopo léxico que também é conhecido como escopo estático. Nesta implementação, as regiões onde os bindings (como as variáveis) são visíveis são definidas pela sua localização no código-fonte! Isto mesmo, ao digitar código Python, já estamos definindo escopos!
Vejamos o exemplo a seguir. Em Python, funções definem seu próprio escopo. Como sabemos que estes são definidos logo no código-fonte, podemos identificá-los apenas com uma análise rápida do código:
Ao analisar o binding do identificador x na linha 3, vemos que no escopo global, x referencia o objeto inteiro 2. Contudo, dentro da função fn, um novo escopo é definido no qual o identificador x referencia o objeto inteiro 10.
Já dentro da função fn2, não há uma redefinição do identificador x. Contudo, como fn2 foi definida dentro de um contexto estático (definido no código) onde x = 10, fn2 pode acessar o binding x que referencia o objeto inteiro 10. De forma análoga, fn pode acessar o binding z já que também foi definida em um contexto onde o identificador z já existia. Portanto, ao invocar fn, printamos o valor -1 que é acessível pois fn foi definida em um contexto onde z = -1 e invocamos fn2 que printa o valor 10 pois definida num contexto onde x = 10.
Assim, o interpretador sempre busca o binding de um identificador no escopo local da função, e caso não o encontre, busca no próximo escopo definido no código-fonte até chegar no escopo global. Caso o binding não seja encontrado em nenhum escopo, uma exceção será gerada.
Vemos, portanto, que o escopo de uma variável depende de sua localização no código-fonte e, assim, dizemos que o escopo de uma variável depende de um contexto léxico (estático).
Este comportamento é o oposto de linguagens que usam um escopo dinâmico (como em Lisp) cuja resolução de nomes dependerá do estado do programa (contexto de execução) e não do contexto estático do programa. Mas este assunto fica para uma postagem futura!
Vou encerrar este post por aqui uma vez que cobrimos dois tópicos muito teóricos e um pouco extensos! No próximo post, veremos como podemos utilizar o escopo léxico para criar closures bem como ver sua definição!
Em caso de dúvidas, erros ou sugestões, por favor entrem em contato! Discussões são sempre bem vindas!
Até o próximo post!