Operações funcionais de coleções em Groovy
Linguagens funcionais são aquelas que consideram as funções como cidadãs de primeira classe. Isso significa que funções podem ser atribuídas a variáveis, passadas e recebidas como parâmetros. Isto permite maior flexibilidade para o desenvolvedor, o que é bastante perceptível no caso da manipulação de coleções. Também permite a redução de linhas de código, o que, como costumo dizer, melhora manutenibilidade dos sistemas. Programação funcional tem ganhado tanta força ultimamente, que até a linguagem Java, criticada pelo conservadorismo na sua evolução, apostou pesado nesta linha, ao permitir expressões lambda, na Java 8.
A primeira linguagem em que vi a utilização prática de operações funcionais foi JavaScript. Por exemplo, os callbacks de chamadas AJAX são funções passadas como parâmetros para a função. Depois, descobri e gostei muito de Scala e de suas capacidades funcionais. Agora, também tenho aproveitado e praticado, em Groovy, as noções de programação funcional que aprendi em JavaScript e Scala.
Em todas as linguagens citadas, é possível passar um código como parâmetro. A maior diferença é que aquilo que o pessoal de Groovy chama de closures, as demais chamam funções anônimas.
Alguns dos métodos que recebem closures em Groovy são os seguintes:
collect: transforma os itens de uma coleção.
find: retorna o primeiro item que atende a condição informada.
findAll: retorna todos os itens que atendem a condição informada.
any: retorna um Boolean indicando se algum dos itens atendem a condição informada.
every: retorna um Boolean indicando se todos os itens atendem a condição informada.
max: retorna o maior valor da coleção, considerando a regra informada para identificar, entre dois valores, qual é o maior.
min: semelhante ao anterior, mas retornando o menor.
sort: retorna a coleção ordenada. Recebe como parâmetro, uma closure com uma regra igual à do método max.
each: executa qualquer código para cada item da coleção.
eachWithIndex: semelhante ao anterior, mas podendo saber a posição do item na coleção.
inject: retorna o resultado de uma operação realizada entre todos os elementos, como uma soma entre todos eles, por exemplo.
A seguir, vou apresentar os métodos acima. Para o primeiro, collect, vou dar um exemplo de código mais tradicional e um utilizando o método. Para os demais, meus exemplos serão apenas dos métodos. Para usar métodos que recebem closures, eles precisam ser chamados com { } (chaves), podendo ou não receber outros parâmetros, que vão entre parênteses.
Transformação de Coleções
Quando temos uma coleção e precisamos transformar cada um de seus itens (por exemplo, se temos uma coleção de objetos e queremos ter uma coleção de mapas criados a partir daqueles objetos), a maneira mais comum de fazer isto é mais ou menos a seguinte:
// considere que "pessoas" é uma coleção de Pessoa, que tem os atribudos nome e idade def lista = [] for (Pessoa p : pessoas) { lista << [nome: p.nome, idade: p.idade] }
Mas temos o método collect, que recebe uma closure de transformação dos objetos da lista, permitindo o seguinte código:
pessoas.collect{ p -> [nome: p.nome, idade: p.idade] }
Neste caso, p refere-se a cada item da coleção. O p poderia ser suprimido, se utilizássemos o it, que é implícito:
pessoas.collect{ [nome: it.nome, idade: it.idade] }
Assim, tem-se um código ainda mais enxuto.
Encontrar itens na coleção
Para encontrar a primeira pessoa com idade maior que 18:
pessoas.find{ it.idade > 18 }
Para encontrar todas as pessoas com idade maior que 18:
pessoas.findAll{ it.idade > 18 }
Verificar se alguma pessoa tem idade maior que 18:
pessoas.any{ it.idade > 18 }
Para verificar se todas as pessoas têm idade maior que 18:
pessoas.every{ it.idade > 18 }
Para encontrar a pessoa com a maior idade:
O exemplo acima não recebe uma closure, mas o método max pode receber, o que é útil para casos que requerem uma lógica mais complexa de identificação de quem é maior. Por exemplo, pode-se precisar verificar quem tem o maior IMC (Índice de Massa Corporal), que é calculado com base em altura, peso e idade. O código abaixo ilustra o método max recebendo uma closure como parâmetro.
pessoas.max{ a, b -> a.nome.compareTo(b.nome) }
Algo importante a destacar é que, mesmo usando { ... } estas funções possuem retorno convencional, como aquelas que usam ( ... ). Assim, é possível acessar atributos e métodos dos objetos retornados normalmente, como neste exemplo:
pessoas.max{ it.idade }.nome
Para ordenar os itens de uma coleção, passamos uma closure com o critério para definir, a cada dois elementos, quem é o maior, tal como no caso das funções min e max. O método sort retorna uma coleção de mesmo tamanho, com os itens ordenados.
Realizar uma operação sobre os itens
Para realizar uma operação aleatória, tal como em um for, podemos utilizar o método each e eachWithIndex. Por exemplo, se quisermos adicionar o pronome de tratamento "Sr(a)." a todas as pessoas de uma coleção:
def pessoas2 = [] pessoas.each{ pessoas2 << "Sr(a). ${it.nome}" }
Lembrando que o exemplo acima, também poderia ser conseguido com o método collect, de forma mais sucinta... ;)
Se precisarmos ter acesso à posição do item na coleção, usamos o método eachWithIndex. No exemplo abaixo, imprimimos os nomes das pessoas, antecedidos por sua posição na coleção.
pessoas.eachWithIndex{ item, index -> println "${index + 1}- ${item.nome}" }
Podemos fazer uma operação cumulativa para os itens da coleção, com o método inject. No exemplo abaixo, calculamos a média das idades das pessoas.
pessoas.inject{ soma, item -> soma + item.idade } / pessoas.size()
Caso desejemos que seja considerada uma idade inicial de 30, podemos passá-la como parâmetro também:
pessoas.inject(30){ soma, item -> soma + item.idade } / pessoas.size()
E o valor que é acumulado não precisa ser apenas numérico. No exemplo abaixo, guardamos as idades em uma nova coleção.
def idades = pessoas.inject([]){ lista, item -> lista << item.idade }
Note que foi necessário inicializar o "acumulador" como sendo uma lista vazia. Daí, cada idade da coleção é adicionada a esta coleção. Lembrando, que, como vimos, esta mesma tarefa poderia ser conseguida com os métodos collect e each.