12 de setembro de 2013

Usando funções anônimas em Delphi

Como o próprio nome diz, uma função anônima é um trecho de código de programação declarado sem um nome, podendo ser armazenado como uma variável e chamado num momento conveniente, de acordo com a necessidade. Essa característica possibilita a criação de algoritmos genéricos que podem ser aplicados em uma ampla gama de situações, independente do tipo de dado envolvido.

As situações mais comumente resolvidas com funções anônimas são as associadas à ordenação de uma lista de valores (sort), seleção de elementos de uma lista que atendam determinadas condições (filtragem) e a aplicação de cálculos envolvendo os valores contidos numa lista.

Muitas linguagens adotaram esse tipo de recurso em suas especificações, tais como C#, C++ 11, JavaScript e Perl. No caso do Delphi, as funções anônimas são um recurso nativo desde a versão 2009.

Para exemplificar o uso desse recurso em Delphi, trabalharemos com uma lista de produtos, realizando com ela operações tais como totalizar o custo dos produtos, aplicar uma porcentagem de aumento nos preços deles e obter uma sub lista dos produtos com características determinadas dinamicamente.

Em qualquer situação, o primeiro passo é definir um tipo de dado para referenciar a função genérica, estabelecendo os parâmetros e o tipo de retorno esperados, caso haja algum. Para somar o custo dos produtos, podemos desenhar uma função recebendo uma instância do produto atual e retornando o custo desse produto:
type
TWProduto = class
{ ... }
public
Codigo: String;
Qtde: double;
VrUnit: double;
CustoUnit: double;
end;

{ Declaração do tipo de dado para referência à função anônima }
TWObtemValorProduto = reference to function (AProd: TWProduto): double;

Com essa declaração feita, podemos montar a função genérica para percorrer uma lista de produtos e somar seus respectivos custos:
function CalculaCusto(Lista: TList<TWProduto>; ObtemValorProd: TWObtemValorProduto) : double;
var lProd : TWProduto;
begin
Result := 0.0;
for lProd in Lista do
Result := Result + ObtemValorProd(lProd);
end;

Observe que o segundo parâmetro é uma referência à uma função, representada pelo tipo que declaramos no primeiro quadro. Ele é usada no código como se fosse uma função comum mas, na realidade, podemos aproveitá-lo para realizar qualquer cálculo com os produtos da lista. Como demonstra o trecho de código a seguir, basta passar uma função anônima com o cálculo desejado:
var Lista: TList<TWProduto>;
soma : double;
begin
Lista := ObtemListaProdutos();

{ função anônima para somar, usando valor unitário }
soma := CalculaCusto (Lista, function (AProd: TWProduto) : double
begin
Result := AProd.Qtde * AProd.VrUnit;
end);
ShowMessage (FloatToStr(soma));

{ Usa outro tipo de cálculo, somando o custo unitário }
soma := CalculaCusto (Lista, function (AProd: TWProduto) : double
begin
Result := AProd.Qtde * AProd.CustoUnit;
end);
ShowMessage (FloatToStr(soma));

O mesmo conceito pode ser aplicado para criar um método de filtragem da lista, construindo uma nova que contenha apenas produtos selecionados. Neste caso, a função deve ter como retorno uma indicação determinando se um produto específico atende ou não o critério estipulado.
type
TWFiltroProduto = reference to function (AProd: TWProduto): boolean;

{ ... }

function Filtro(Lista: TList<TWProduto>; AFiltro: TWFiltroProduto) : TList<TWProduto>;
var lProd : TWProduto;
begin
Result := TList<TWProduto>.Create;

for lProd in Lista do
{ Se esse produto atende o critério, adiciona-o na lista de retorno }
if AFiltro (lProd) then
Result.Add(lProd);
end;

{ ... }
procedure TrataLista;
var Lista, ListaCaros: TList<TWProduto>;
begin
Lista := ObtemListaProdutos();

ListaCaros := Filtro(Lista, function (AProd: TWProduto) : boolean
begin
{ Filtra produtos custando mais que 5 reais }
Result := (AProd.VrUnit > 5.00);
end);
{ ... }
ListaCaros.Free;
Lista.Free;
end;

Observe que o algoritmo em si é genérico o suficiente para receber qualquer tipo de dado. Ou seja, poderíamos usar TObjectList no lugar da coleção TList, trocar o TWProduto por TObject e usar a função de filtro com qualquer classe do Delphi.

Pelo que vimos, o objetivo das funções anônimas é permitir a personalização da resposta a uma determinada ocorrência dentro de seu programa. Mas essa não é justamente a definição de eventos em Delphi e em outros ambientes de programação?

Há algumas diferenças entre responder a um evento e trabalhar com uma função anônima. Um evento normalmente está associado à instância de um componente. A resposta a ele exige, portanto, o uso de um ponteiro duplo: um para indicar qual objeto responderá ao evento e o outro para a função (nomeada) desse objeto que deve ser chamada. Já uma função anônima é somente um ponteiro para função, independente da existência de um objeto para funcionar.

Outra diferença importante está no escopo das variáveis. No caso do evento, as variáveis disponíveis são as mesmas acessíveis pelo objeto que está providenciando uma resposta, isto é, a resposta pode acessar os membros do objeto, as variáveis globais e os parâmetros fornecidos pela assinatura do evento. Já uma função anônima consegue enxergar também as variáveis locais existentes no ponto em que ela foi definida.

Para exemplificar essa última afirmação, vamos criar uma função que varra a lista aplicando uma regra a cada produto. A regra, que é passada como parâmetro, será o aumento no valor de cada produto usando uma porcentagem estipulada fora da função anônima:
{Processa a lista de produtos, aplicando a cada um os cálculos definidos no parâmetro DoProcProd }
procedure ProcessaProduto(Lista: TList<TWProduto>; DoProcProd: TWObtemValorProduto);
var lProd : TWProduto;
begin
for lProd in Lista do
DoProcProd(lProd);
end;

procedure TrataLista;
var Lista: TList<TWProduto>;
PorcCorrecao: double;
RegraCorrecao : TWObtemValorProduto;
begin
{ ... }
PorcCorrecao := 6.52; { Aumentar 6.52 % }
RegraCorrecao := function (AProd: TWProduto) : double
begin
AProd.VrUnit := AProd.VrUnit * (1+(PorcCorrecao/100.0));
Result := AProd.VrUnit;
end;
ProcessaProduto(Lista, RegraCorrecao);
{ ... }
end;
O trecho de código acima também mostra que é possível armazenar uma função anônima numa variável e aplicar exatamente a mesma regra em outros pontos do programa onde forem necessárias.

No próximo post eu mostro como usar funções anônimas para flexibilizar a serialização de objetos JSON, isto é, como personalizar a leitura e gravação de objetos no formato JSON para troca de informações entre programas, especialmente no ambiente web.

Mais Informações
Anonymous functions

2 comentários :

Unknown disse...

Cara... trabalho com delphi a tanto tempo (desde 2005), e não conhecia isso. Obrigado Luís.

Unknown disse...

Muito boa a dica, sempre há algo novo para se aprender.

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.