Back-End

30 ago, 2012

Pensamento Funcional: recursos funcionais no Groovy – Parte 01

Publicidade

Confissão: nunca mais quero trabalhar em uma linguagem que não seja coletora de lixo. Já trabalhei muito em linguagens como C++ e não quero ceder às conveniências das linguagens modernas. Essa é a história de como o desenvolvimento de software progride. Desenvolvemos camadas de abstração para lidar (e ocultar) detalhes rotineiros. À medida que a capacidade dos computadores cresceu, transferimos mais tarefas para as linguagens e tempos de execução. Recentemente, há uma década mais precisamente, os desenvolvedores evitavam linguagens interpretadas por serem muito lentas para aplicativos de produção, mas hoje elas são comuns. Há uma década, muitos dos recursos das linguagens funcionais eram extremamente lentos, mas fazem todo sentido agora, pois otimizam o tempo e o esforço do desenvolvedor.

Muitos dos recursos abordados nesta série de artigos mostram como as linguagens e estruturas funcionais lidam com detalhes rotineiros. No entanto, você não precisa recorrer a uma linguagem funcional para se beneficiar dos desenvolvimentos funcionais. Neste artigo e no próximo, mostrarei como algumas programações funcionais já fazem parte do Groovy.

Listas funcionais do Groovy

O Groovy aumenta consideravelmente as bibliotecas de coleção Java, incluindo a adição de desenvolvimentos funcionais. A primeira funcionalidade do Groovy é fornecer uma perspectiva diferente sobre as listas, o que parece comum a princípio, mas oferece alguns benefícios interessantes.

Vendo as listas de forma diferente

Se você tem experiência com linguagens C ou parecidas com C (incluindo Java), provavelmente, você conceitualiza as listas como coleções indexadas. Essa perspectiva facilita a iteração sobre uma coleção, mesmo quando você não usa explicitamente o índice, como mostra o código do Groovy na Listagem 1:

def perfectNumbers = [6, 28, 496, 8128]

def iterateList(listOfNums) {
  listOfNums.each { n ->
    println "${n}"
  }
}

iterateList(perfectNumbers)

O Groovy também inclui um iterador eachWithIndex() que fornece o índice como um parâmetro para o bloco de códigos para casos nos quais é necessário o acesso explícito. Apesar de não usar um índice no método iteraterList() na Listagem 1, ainda penso nele como uma coleção organizada de slots, como mostra a Figura 1:

Muitas linguagens funcionais têm uma perspectiva um pouco diferente sobre as listas e, felizmente, o Groovy compartilha essa perspectiva. Em vez de pensar em uma lista como slots indexados, pense nela como uma combinação do primeiro elemento na lista (o início) com o restante da lista (o fim), como mostra a Figura 2:

Pensar em uma lista com início e fim permite que eu itere usando a recursão, como mostra a Listagem 2:

def recurseList(listOfNums) {
if (listOfNums.size == 0) return;
println "${listOfNums.head()}"
recurseList(listOfNums.tail())
}

recurseList(perfectNumbers)

No método recurseList() na Listagem 2, primeiramente, verifiquei se a lista passada como o parâmetro não contém elementos. Se esse for o caso, poderei retornar. Caso contrário, imprimo o primeiro elemento na lista, disponível por meio do método head() do Groovy e chamo o método recurseList() de forma recursiva no restante da lista.

A recursão tem limites técnicos integrados à plataforma (consulte Recursos), portanto, isso não é uma solução geral. Mas deve funcionar para listas que contêm um pequeno número de itens. Estou mais interessado em investigar o impacto sobre a estrutura do código, antecipando o dia em que os limites serão facilitados ou desaparecerão. Graças a algumas falhas, talvez o benefício da versão recursiva não seja imediatamente óbvio. Para torná-lo mais óbvio, considere o problema de filtragem de uma lista. Na Listagem 3, mostro um exemplo de um método de filtragem que aceita uma lista e um predicado (um teste booleano) a fim de determinar se o item pertence à lista:

def filter(list, p) {
def new_list = []
list.each { i ->
if (p(i))
new_list << i
}
new_list
}

modBy2 = { n -> n % 2 == 0}

l = filter(1..20, modBy2)

O código na Listagem 3 é bastante direto: crio uma variável portadora para os elementos que desejo manter, itero sobre a lista, verifico cada elemento com o predicado de inclusão e retorno a lista de itens filtrados. Quando chamo filter(), forneço um bloco de códigos que especifica os critérios de filtragem.

Considere uma implementação recursiva do método de filtragem da Listagem 3, mostrado na Listagem 4:

def filter(list, p) {
if (list.size() == 0) return list
if (p(list.head()))
[] + list.head() + filter(list.tail(), p)
else
filter(list.tail(), p)
}

l = filter(1..20, {n-> n % 2 == 0})

No método filter() , na Listagem 4, primeiro verifico o tamanho da lista passada e, se não houver elementos, a retorno. Caso contrário, verifico o início da lista com base em meu predicado de filtragem. Se passar, eu adiciono à lista (com uma lista vazia inicial para ter certeza de que sempre retorno o tipo correto), caso contrário, eu filtro o fim.

A diferença entre a Listagem 3 e a Listagem 4 destaca uma questão importante: quem se importa com o estado? Na versão imperativa, eu me importo. Eu preciso criar uma nova variável chamada new_list, e preciso adicionar coisas a ela e retorná-la após a conclusão. Na versão recursiva, a linguagem gerencia o valor de retorno, desenvolvendo-o sobre a pilha como o retorno recursivo de cada invocação de método. Perceba que cada rota de saída do método filter() na Listagem 4 é uma chamada de retorno, que desenvolve o valor intermediário sobre a pilha.

Embora não seja uma melhoria tão importante quanto a coleta de lixo, ilustra uma tendência importante em linguagens de programação: a transferência de partes em movimentação. Se eu nunca puder acessar os resultados intermediários da lista, não poderei apresentar bugs na maneira em que interajo com ela.

Essa mudança de perspectiva com relação às listas permite que você explore outros aspectos, como o tamanho e o escopo da lista.

Listas lentas no Groovy

Um dos recursos comuns das linguagens funcionais é a lista lenta: uma lista cujo conteúdo é gerado somente quando você precisa dele. Essas listas permitem que você adie a inicialização de recursos dispendiosos até que eles sejam absolutamente necessários. Elas também permitem a criação de sequências infinitas: listas sem limite superior. Se não for necessário saber logo no início o tamanho da lista, é possível deixá-la com o tamanho que desejar.

Primeiramente, mostrarei um exemplo de como usar uma lista lenta no Groovy na Listagem 5 e depois sua implementação:

def prepend(val, closure) { new LazyList(val, closure) }

def integers(n) { prepend(n, { integers(n + 1) }) }

@Test
public void lazy_list_acts_like_a_list() {
def naturalNumbers = integers(1)
assertEquals('1 2 3 4 5 6 7 8 9 10',
naturalNumbers.getHead(10).join(' '))
def evenNumbers = naturalNumbers.filter { it % 2 == 0 }
assertEquals('2 4 6 8 10 12 14 16 18 20',
evenNumbers.getHead(10).join(' '))
}

O primeiro método na Listagem 5, prepend(), cria um novo LazyList, permitindo que você anexe previamente os valores. O próximo método, integers(), retorna uma lista de números inteiros usando o método prepend() . Os dois parâmetros enviados ao método prepend() são: o valor inicial da lista e um bloco de código que inclui um código para gerar o próximo valor. O método integers() age como uma fábrica que retorna a lista lenta de números inteiros com um valor na frente e uma forma de calcular valores adicionais na parte de trás.

Para recuperar os valores da lista, eu chamo o método getHead() , o que retorna o número do argumento de valores da parte superior da lista. Na Listagem 5, naturalNumbers é uma sequência lenta de todos os números inteiros. Para obter alguns deles, eu chamo o método getHead() , especificando quantos números inteiros desejo. Como indica a asserção, eu recebo uma lista dos 10 primeiros números naturais. Usando o método filter() , eu recupero uma lista lenta de números pares e chamo o método getHead() para obter os 10 primeiros números pares.

A implementação da LazyList é exibida na Listagem 6:

class LazyList {
private head, tail

LazyList(head, tail) {
this.head = head;
this.tail = tail
}

def LazyList getTail() { tail ? tail() : null }

def List getHead(n) {
def valuesFromHead = [];
def current = this
n.times {
valuesFromHead << current.head
current = current.tail
}
valuesFromHead
}

def LazyList filter(Closure p) {
if (p(head))
p.owner.prepend(head, { getTail().filter(p) })
else
getTail().filter(p)
}
}

Uma lista lenta possui um início e um fim que são especificados no construtor. O método getTail() garante que o fim não seja nulo e a executa. O método getHead() reúne os elementos que desejo retornar, um por vez, obtendo o elemento existente do início da lista e pedindo ao fim para gerar um novo valor. A chamada para n.times {} realiza essa operação para o número de elementos solicitados e o método retorna os valores colhidos.

O método filter() , na Listagem 5 usa a mesma abordagem recursiva que a Listagem 4 , mas a implementa como parte da lista e não como uma função autônoma.

As listas lentas estão presentes em Java (consulte Recursos), mas são muito mais fáceis de implementar em linguagens com recursos funcionais. As listas lentas funcionam muito bem em situações nas quais a geração de recursos é dispendiosa, como na obtenção de listas de números perfeitos.

Lista lenta de números perfeitos

Se você acompanha esta série de artigos, conhece meu código teste favorito, localizar números perfeitos (consulte Pensando funcionalmente – Parte 01)”). Até agora, uma das desvantagens de todas as implementações é a necessidade de especificar o número para classificação. Em vez disso, desejo uma versão que retorna uma lista lenta de números perfeitos. Para isso, escrevi um localizador de números perfeitos altamente funcional e muito compacto que suporta listas lentas. Veja na Listagem 7:

class NumberClassifier {
static def factorsOf(number) {
(1..number).findAll { i -> number % i == 0 }
}

static def isPerfect(number) {
factorsOf(number).inject(0, {i, j -> i + j}) == 2 * number
}

static def nextPerfectNumberFrom(n) {
while (! isPerfect(++n)) ;
n
}
}

Se o código nos métodos factorsOf() e isPerfect() parecer difícil, você poderá ver a derivação desses métodos no último artigo. O novo método, nextPerfectNumber(), usa o método isPerfect() para encontrar o próximo número perfeito além do número passado como parâmetro. Essa chamada de método irá demorar muito para executar valores pequenos (especialmente devido à falta de otimização do código), simplesmente não há tantos números perfeitos assim.

Usando essa nova versão do NumberClassifier, posso criar uma lista lenta de números perfeitos, como mostra a Listagem 8:

def perfectNumbers(n) {
prepend(n,
{ perfectNumbers(nextPerfectNumberFrom(n)) }) };

@Test
public void infinite_perfect_number_sequence() {
def perfectNumbers = perfectNumbers(nextPerfectNumberFrom(1))
assertEquals([6, 28, 496], perfectNumbers.getHead(3))
}

Usando o método prepend() , definido na Listagem 5, desenvolvi uma lista de números perfeitos com o valor inicial como início e um bloco de fechamento que sabe como calcular o próximo número perfeito como fim. Eu inicializo minha lista com o primeiro número perfeito depois de 1 (usando uma importação estática para que eu possa chamar meu método NumberClassifier.nextPerfectNumberFrom() com mais facilidade) e peço que minha lista retorne os três primeiros números perfeitos.

O cálculo de novos números perfeitos é dispendioso, portanto eu o faria o mínimo possível. Ao desenvolver como uma lista lenta, posso adiar os cálculos até o momento exato.

É mais difícil pensar sobre as sequências infinitas se sua abstração de “lista” for “slots numerados”. Pensar em uma lista como o “primeiro elemento” e o “restante da lista” o incentiva a pensar sobre os elementos na lista em vez de pensar na estrutura, o que, por sua vez, permite que você pense sobre coisas como listas lentas.

Conclusão

Uma das maneiras de os desenvolvedores darem grandes saltos em sua produtividade é o desenvolvimento de abstrações efetivas para ocultar os detalhes. Não chegaríamos a lugar algum se ainda estivéssemos programando com uns e zeros. Um dos aspectos cativantes das linguagens funcionais é a tentativa de abstrair mais detalhes dos desenvolvedores. Linguagens dinâmicas modernas no JVM já proporcionam muitos desses recursos. Neste artigo, eu mostrei como mudar sua perspectiva sobre como o desenvolvimento das listas permite que a linguagem gerencie o estado durante a iteração. Além disso, quando você pensa em listas como “início” e “fim”, consegue desenvolver listas lentas e sequências infinitas.

No próximo artigo, você verá como a metaprogramação do Groovy pode tornar seus programas mais funcionais — e permite que você aumente as bibliotecas funcionais de terceiros para que elas trabalhem melhor no Groovy.

***

O IBM® Domino® Symphony é um conjunto de software de produtividade para escritórios para criação, edição e compartilhamento de documentos de processamento de texto, planilhas e apresentações, disponível para download grátis. Projetado para lidar com a maioria das tarefas que os usuários finais executam, o software IBM Symphony oferece suporte ao OpenDocument Format (ODF), permitindo que as organizações possam acessar, usar e manter seus documentos a longo prazo sem se preocuparem com incertezas de fim da vida útil ou licenciamento de software contínuo e taxas de royalty.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

***

Autor: Neal Ford é um arquiteto de software e Meme Wrangler, na ThoughtWorks, uma consultoria global de TI. Projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD, e é autor ou editor de livros que abordam uma variedade de tecnologias, inclusive The Productive Programmer Seu enfoque é o projeto e construção de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu Web site.

***

Artigo original disponível em: http://www.ibm.com/developerworks/br/library/j-ft7/index.html