EPx professional blog and repository for braindumps

2008/12/07

O tal do "closure"

Com a recente ressurgência das linguagens interpretadas, o tal do "closure" está sendo mencionado o tempo todo, em particular no contexto do Ruby. Mas afinal, que bicho é esse? É um conceito difícil de pegar para quem vem de um background C/C++, e também foi difícil para mim muito embora existiam closures no Clipper 5, e eu cheguei a mexer em código com um closure. Mas vamos lá.

Um closure simples em Python poderia ser criado e usado das formas abaixo:

>>> a = lambda a: a*2
>>> a(3)
6

>>> def dobro(x):
... return x*2
...
>>> a = dobro
>>> a(3)
6

Na primeira forma, usamos o operador "lambda" e criamos o que muita gente chama de função anônima, ou sem nome, que fica contida em uma variável. Na segunda forma, atribuímos uma função convencional a uma variável, com o mesmo resultado.

Estas mesmas coisas poderiam ser feitas facilmente em Javascript, pois também nele as funções e métodos são "objetos de primeira classe" e podem ser atribuídos e passados como se fossem dados.

Na verdade, também poderíamos fazer as mesmas coisas em C/C++ usando ponteiros para funções. Isto leva muita gente a concluir que um closure é simplesmente uma função anônima ou um ponteiro para função ou método, mas closures são capazes de um truque adicional.

Considere o seguinte código Ruby e Clipper 5:

# Ruby
?> def multiplica x
>> lambda {|y| x*y}
>> end
=> nil
>> x2 = multiplica 2
=> #<Proc:0x005bf0f4@(irb):78>
>> x10 = multiplica 10
=> #<Proc:0x005bf0f4@(irb):78>
>> x2(3)
NoMethodError: undefined method `x2' for main:Object
from (irb):81
>> x2.call(3)
=> 6
>> x10.call(3)
=> 30


# Clipper 5
function multiplica(x)
return {|y| return x*y}

x2 = multiplica(2)
x10 = multiplica(10)
? eval(x2, 3)
6
? eval(x10, 3)
30

Tanto no código Ruby como no Clipper5, a função "multiplica" não retorna mais um resultado numérico, mas sim uma outra função, que pode ser reutilizada tantas vezes quanto necessário. Agora, o mais importante: cada função retornada CONGELA o parâmetro "x" originalmente recebido. Portanto, "x2" sempre vai multiplicar números por 2, pois 2 foi o valor recebido por "x" por ocasião da criação do closure x2. Podemos criar tantos closures quanto quisermos, e cada um terá a sua versão congelada de "x", sem interferência mútua.

Explicando de outra forma, o closure "tira uma fotografia" do estado das variáveis locais no momento de sua criação, e baseia-se nesta "fotografia" quando, mais tarde, sua execução for requisitada.

Esta característica única dos closures distingue-os dos simples ponteiros a funções do C/C++. Os closures têm um quê de templates do C++, embora sejam infinitamente mais poderosos por serem criados em tempo de execução.

É notório também que tanto no Ruby quanto no Clipper 5 a avaliação do closure/code block demanda um método especial (call e eval, respectivamente). Tentar chamar o closure diretamente dá erro.

Isto acontece pois funções não são objetos de primeira classe nestas linguagens, e têm de ser encapsuladas para poderem ser manipuladas. No caso do Ruby, code blocks precedidos de "lambda" são convertidos para um objeto tipo Proc, que responde ao método call.

Embora Python não deixe isto muito óbvio, também é possível usar closures usando funções internas. Em Python, funções são objetos de primeira classe e closures podem ser chamados como qualquer função. Vide exemplo:

>>> def multiplica(x):
... def closure(y):
... return x*y
... return closure
...
>>> x2 = multiplica(2)
>>> x10 = multiplica(10)
>>> x2(3)
6
>>> x10(3)
30


A primeira vista, a função closure no código acima parece simplesmente uma função de uso privativo de multiplica, mas é potencialmente um closure que poderá carregar o valor de "x' com ele.

O closure, enquanto ferramenta embutida na linguagem, só é possível se a linguagem é interpretada e possui coletor de lixo, pois o closure retêm a "fotografia" das variáveis locais para além do seu escopo original, e só pode descartar a fotografia quando o closure for ele mesmo descartado. Além disso, precisa referir-se às variáveis pelo nome e não como endereços fixos de memória, já que as variáveis originais já não existirão.

Seria possível emular um closure simples em C++ usando functors (objetos que respondem ao operador (), ou seja, podem ser chamados como funções), como no exemplo a seguir:

#include <stdio.h>

class Multiplica {
public:
int xx;
Multiplica(int x) { xx = x; }
int operator() (int y) { return xx*y; }

};

int main()
{
Multiplica x2 = Multiplica(2);
Multiplica x10 = Multiplica(10);
printf("%d %d\n", x2(3), x10(3));
}

/Users/epx $ gcc -o closure closure.cpp -lstdc++
/Users/epx $ ./closure
6 30

Um grande problema aqui é a falta de naturalidade: o desenvolvedor do functor Multiplica precisa tomar conta do armazenamento de todas as "variáveis locais" de que ele vá precisar posteriormente.

Os closures têm diversas utilidades. Em geral, quem vem de um background C/C++ tende a ver os closures como ponteiros para funções, e usa closures exclusivamente desta forma. É o meu caso. Ainda assim é um uso extremamente importante -- por exemplo, absolutamente toda implementação de sort() aceita uma função ou closure como parâmetro, que será responsável pela comparação entre elementos.

Outro uso é o o encapsulamento de tarefas (código + parâmetros de execução) para posterior execução. Quem ordena a execução do closure não precisa saber absolutamente nada sobre ela. Nada impede que a tarefa receba ainda parâmetros adicionais no momento da execução. Imagino também que os "decorators" do Python sejam implementados através de closures.

Um uso mais esotérico, que não envolve fotografia de variáveis nem fazer papel de ponteiro de função, é a criação de novos "comandos" numa linguagem. Isto faz mais sentido onde a sintaxe do code block integra-se bem com o resto da linguagem, como no caso do Ruby:

def executar_as_vezes(&codeblock)
if rand < 0.3
codeblock call
end
end

executar_as_vezes {
...
}

# Também podemos fazer o bloco assim:
executar_as_vezes do
....
end

Tal uso de closure seria difícil em Python pois uma função lambda não ficaria "natural". Em Javascript, o resultado ficaria mais natural que Python (podemos passar um bloco de código entre chaves) porém menos natural que Ruby pois o bloco tem de ser precedido por function().

Este uso de closures é mais importante em linguagens como LISP e Smalltalk -- se bem entendi, até estruturas de controle como if, while etc. são na verdade funções implementadas na própria linguagem, da mesma forma que implementamos executar_as_vezes.

Em Ruby, o idioma de passar bloco de código para método é muito usado para controlar o commit/release de recursos. Considere os seguintes casos:

File.open("bla.txt") do |f|
# trabalha com o arquivo na variável f
end

Database.Transaction do |t|
# trabalha com o banco de dados dentro
# do contexto da transação t
end

Embora não esteja imediatamente óbvio (para quem não conhece Ruby), os blocos de código do...end são passados como parâmetros aos métodos File.open e Database.Transaction. Os blocos somente serão executados se os métodos File.open e Database.Transaction assim o quiserem, quando quiserem, quantas vezes quiserem.

Isto permite que os métodos gerenciem os recursos de forma transparente e automática -- no caso, encerrar a transação ou fechar o arquivo depois de executado o bloco. Talvez até mesmo tentar o bloco novamente se a transação for abortada!

0 comentários:

Postar um comentário