Back-End

3 set, 2012

Pensamento Funcional: recursos funcionais no Groovy – Parte 02

Publicidade

No último artigo, mostrei alguns dos recursos funcionais originais do Groovy e como usar as primitivas do Groovy para desenvolver uma lista infinita. Neste artigo, continuo minha exploração da intersecção da programação funcional e do Groovy.

Groovy é uma linguagem de diversos paradigmas: ela oferece suporte à orientação de objeto, metaprogramação e estilos de programação funcional, que são em sua maioria ortogonais com relação ao outro (consulte  o verbete Ortogonalidade ao fim do texto). A metaprogramação permite que você adicione recursos a uma linguagem e suas bibliotecas principais. Ao combinar a metaprogramação com a programação funcional, é possível tornar seu próprio código mais funcional ou aumentar bibliotecas funcionais de terceiros para que funcionem melhor no Groovy. Primeiro, mostrarei como funciona o ExpandoMetaClass do Groovy a fim de aumentar as classes e, depois, como usar esse mecanismo para integrar a biblioteca Functional Java (consulte Recursos) ao Groovy.

Classes abertas via ExpandoMetaClass

Um dos recursos mais eficientes do Groovy é a classe aberta, a capacidade de reabrir uma classe existente a fim de aumentar ou remover sua funcionalidade. Isso é diferente de definir como subclasse, pelo fato de um novo tipo ser obtido a partir de um existente. As classes abertas permitem que você reabra uma classe como uma cadeia de caracteres e adicione novos métodos a ela. As bibliotecas de teste usam bastante esse recurso para aumentar o Objeto com métodos de verificação, de modo que todas as classes em um aplicativo tenham os métodos de verificação.

O Groovy tem duas técnicas de classe aberta: categorias e ExpandoMetaClass (consulte Recursos). As duas funcionarão para este exemplo; escolhi ExpandoMetaClass , pois é sintaticamente mais simples.

Se você está acompanhando esta série, estará familiarizado com meu exemplo recorrente de classificação de número. O Classifier completo no Groovy, exibido na Listagem 1, use os desenvolvimentos funcionais do próprio Groovy:

class Classifier {
  def static isFactor(number, potential) {
    number % potential == 0;
  }

  def static factorsOf(number) {
    (1..number).findAll { i -> isFactor(number, i) }
  }

  def static sumOfFactors(number) {
    factorsOf(number).inject(0, {i, j -> i + j})
  }

  def static isPerfect(number) {
    sumOfFactors(number) == 2 * number
  }

  def static isAbundant(number) {
    sumOfFactors(number) > 2 * number
  }

  def static isDeficient(number) {
    sumOfFactors(number) < 2 * number
  }

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

Se você tiver alguma dúvida sobre como os métodos são implementados nesta versão, consulte os artigos anteriores (em particular, “Acoplamento e composição – Parte 02” e “Recursos funcionais no Groovy – Parte 01“). Para usar os métodos desta classe, posso chamar os métodos da maneira funcional “normal”: Classifier.isPerfect(7). No entanto, com a metaprogramação, posso “conectar” esses métodos diretamente na classe Integer , permitindo que eu “pergunte” a um número em qual categoria ele está.

Para adicionar esses métodos à classe Integer , eu acesso a propriedade metaClass da classe – predefinida pelo Groovy para cada classe – como mostra a Listagem 2:

Integer.metaClass.isPerfect = {->
  Classifier.isPerfect(delegate)
}

Integer.metaClass.isAbundant = {->
  Classifier.isAbundant(delegate)
}

Integer.metaClass.isDeficient = {->
  Classifier.isDeficient(delegate)
}

Na Listagem 2, adicionei três métodos de Classifier ao Integer. Agora, todos os números inteiros no Groovy têm esses métodos. O Groovy não tem noção de tipos de dados primitivos; até mesmos constantes no Groovy usam Integer como o tipo de dados subjacente. No bloco de código que define cada método, tenho acesso ao parâmetro delegate predefinido, que representa o valor do objeto que está invocando o método na classe.

Depois de inicializar meus métodos de metaprogramação (consulte o verbete “Inicializando os métodos de metaprogramação” ao fim do texto), posso “perguntar” aos números sobre as categorias, como mostra a Listagem 3:

@Test
void metaclass_classifiers() {
  def num = 28
  assertTrue num.isPerfect()
  assertTrue 7.isDeficient()
  assertTrue 6.isPerfect()
  assertTrue 12.isAbundant()
}

A Listagem 3 ilustra os métodos recém-adicionados trabalhando em variáveis e constantes. Agora, seria trivial adicionar um método ao Integer que retorna a classificação de um número específico, talvez como uma enumeração.

A adição de novos métodos às classes existentes não é por si só particularmente “funcional”, mesmo se o código que eles chamam for altamente funcional. No entanto, a capacidade de adicionar métodos de forma perfeita facilita a incorporação de bibliotecas de terceiros – como a biblioteca Functional Java – que adiciona recursos funcionais consideráveis. Implementei o classificador de número usando a biblioteca Functional Java no segundo artigo e o usarei aqui para criar um fluxo infinito de números perfeitos.

Mapeando os tipos de dados com a metaprogramação

O Groovy é essencialmente um dialeto de Java, portanto, usar bibliotecas de terceiros como a Functional Java é trivial. No entanto, posso integrar ainda mais essas bibliotecas ao Groovy realizando algum mapeamento de metaprogramação entre os tipos de dados para tornar os pontos menos visíveis. O Groovy tem um tipo de encerramento nativo (usando a classe Closure ). A Functional Java ainda não tem o luxo dos encerramentos (ela depende da sintaxe de Java 5), forçando os autores a usar genéricos e uma classe F geral que contém um método f() . Usando ExpandoMetaClass do o Groovy, posso resolver as diferenças entre o método/tipo de encerramento criando métodos de mapeamento entre os dois.

A classe que eu desejo aumentar é a classe Stream da Functional Java, que fornece uma abstração para listas infinitas. Desejo poder passar encerramentos do Groovy no lugar de instâncias F de Functional Java, portanto, adiciono métodos sobrecarregados à classe Stream a fim de mapear os encerramentos no método f() de F , como mostra a Listagem 4:

Stream.metaClass.filter = { c -> delegate.filter(c as fj.F) }
//    Stream.metaClass.filter = { Closure c -> delegate.filter(c as fj.F) }
Stream.metaClass.getAt = { n -> delegate.index(n) }
Stream.metaClass.getAt = { Range r -> r.collect { delegate.index(it) } }

A primeira linha cria um método filter() no Stream que aceita um encerramento (o parâmetro c do bloco de código). A segunda linha (comentada) é igual à primeira, mas com o acréscimo da declaração de tipo para o Closure; isso não afeta o modo como o Groovy executa o código, mas pode ser mais desejado como documentação. O corpo do bloco de códigos chama o método pré-existente filter()de Stream , mapeando o encerramento do Groovy para a classe fj.F de Functional Java. Eu uso um operador semimágico do Groovy, as , para realizar o mapeamento.

O operador as do Groovy coage os encerramentos em definições de interface, permitindo que os métodos de encerramento mapeiem para os métodos exigidos pela interface. Considere o código na Listagem 5:

def h = [hasNext : { println "hasNext called"; return true},
         next : {println "next called"}] as Iterator

h.hasNext()
h.next()
println "h instanceof Iterator? " + (h instanceof Iterator)

No exemplo da Listagem 5, eu criei um hash com dois pares nome-valor. Cada um dos nomes é uma cadeia de caractere (o Groovy não exige que as chaves hash sejam delimitadas com aspas duplas, pois elas são cadeias de caractere por padrão) e os valores são blocos de código. O operador as mapeia esse hash até a interface Iterator , que exige os métodos hasNext() e next() . Depois de realizar o mapeamento, posso tratar o hash como um agente iterativo, a última linha da listagem mostra true. Em casos em que tenho uma interface de método único ou quando desejo que todos os métodos na interface mapeiem para um único encerramento, posso dispensar o hash e usar as diretamente para mapear um encerramento em uma função. Observando novamente a primeira linha da Listagem 4, eu mapeio o encerramento passado para a classe F de método único. Na Listagem 4, preciso mapear ambos os métodos getAt (um que aceite um número e outro que aceite um Range), pois filter precisa desses métodos para operar.

Usando esse Stream recém-aumentado, posso brincar com uma sequência infinita, como mostra a Listagem 6:

@Test
void adding_methods_to_fj_classes() {

  def evens = Stream.range(0).filter { it % 2 == 0 }
  assertTrue(evens.take(5).asList() == [0, 2, 4, 6, 8])
  assertTrue(evens[3..6] == [6, 8, 10, 12])
}

Na Listagem 6, eu crio uma lista infinita de números inteiros pares, começando com 0, filtrando-os com um bloco de encerramento. Não é possível obter toda a sequência infinita de uma vez, portanto, você precisa take() o máximo de elementos que conseguir. O restante da Listagem 6 mostra asserções de teste que demonstram como o fluxo funciona.

Fluxos infinitos no Groovy

No último artigo, mostrei como implementar uma lista infinita no Groovy. Em vez de criá-la manualmente, por que não depender de uma sequência infinita da Functional Java?

Para criar um Stream infinito de números perfeitos, preciso de dois mapeamentos adicionais do método Stream para entender os encerramentos do Groovy, como mostra a Listagem 7:

Stream.metaClass.asList = { delegate.toCollection().asList() }
Stream.metaClass.static.cons = { head, closure -> delegate.cons(head, closure as fj.P1) }
// Stream.metaClass.static.cons =
//  { head, Closure c -> delegate.cons(head, ['_1':c] as fj.P1)}

Na Listagem 7, criei um método de conversão asList() para facilitar a conversão de um fluxo de Functional Java para uma lista. O outro método que implemento é um cons()sobrecarregado, que é o método em Stream que desenvolve uma nova lista. Ao criar uma lista infinita, a estrutura de dados normalmente contém um primeiro elemento e um bloco de encerramento como a cauda da lista, que gera o próximo elemento, quando invocado. Para meu fluxo do Groovy de números perfeitos, eu preciso de Functional Java para entender que cons() pode aceitar um encerramento do Groovy.

Se eu usar as para mapear um único encerramento até uma interface que possui diversos métodos, esse encerramento será executado para qualquer método que eu chame na interface. Esse estilo de mapeamento simples funciona na maioria dos casos para classes de Functional Java. No entanto, alguns métodos exigem um método fj.P1 em vez de um método fj.F . Em alguns desses casos, ainda posso conseguir um mapeamento simples, pois os métodos de recebimento de dados não dependem de qualquer um dos outros métodos de P1. Em casos nos quais é necessário ter mais precisão, talvez eu tenha que usar o mapeamento mais complexo exibido na linha comentada da Listagem 7, que precisa criar um hash com o método _1() mapeado para o encerramento. Embora esse método pareça estranho, é um método padrão na classe fj.P1 que retorna o primeiro elemento.

Assim que eu tiver meus métodos mapeados metaprogramaticamente no Stream, poderei usar o Classifier da Listagem 1 para criar um fluxo infinito de números perfeitos, como mostra a Listagem 8:

import static fj.data.Stream.cons
import static com.nealford.ft.metafunctionaljava.Classifier.nextPerfectNumberFrom

def perfectNumbers(num) {
  cons(nextPerfectNumberFrom(num), { perfectNumbers(nextPerfectNumberFrom(num))})
}

@Test
void infinite_stream_of_perfect_nums_using_functional_java() {
  assertEquals([6, 28, 496], perfectNumbers(1).take(3).asList())
}

Eu uso importações estáticas para cons() do Functional Java e para meu próprio método nextPerfectNumberFrom() do Classifier para tornar o código menos detalhado. O método perfectNumbers() retorna uma sequência infinita de números perfeitos alocando (sim, alocar é um verbo) o primeiro número perfeito após o número inicial como o primeiro elemento e adicionando um bloco de encerramento como o segundo elemento. O bloco de encerramento retorna a sequência infinita com o próximo número como a cabeça e o encerramento para calcular o outro como a cauda. No teste, gerei um fluxo de números perfeitos começando com 1, obtendo os três próximos números e assegurando que eles correspondem à lista.

 Conclusão

Quando os desenvolvedores pensam em metaprogramação, eles normalmente pensam somente em seu próprio código, não em aumentar o de outra pessoa. O Groovy permite que eu adicione novos métodos não apenas a classes integradas como Integer, mas também a bibliotecas de terceiros como Functional Java. A combinação de metaprogramação e programação funcional gera um grande poder com muito pouco código, criando um link perfeito.

Embora eu chame as classes de Functional Java diretamente do Groovy, muitos dos blocos de construção da biblioteca são desajeitados comparados com encerramentos reais. Ao usar a metaprogramação, posso mapear os métodos de Functional Java a fim de permitir que eles entendam as estruturas de dados convenientes do Groovy, conseguindo o melhor de ambos os mundos. Até que o Java defina um tipo de encerramento nativo, os desenvolvedores precisam realizar com frequência esses mapeamentos poliglotas entre os tipos de linguagem: um encerramento do Groovy e um encerramento Scala não são a mesma coisa no nível de bytecode. Ter um padrão em Java colocará essas conversas no tempo de execução e eliminará a necessidade de mapeamentos como aqueles que mostrei aqui. Até que isso aconteça, no entanto, esse recurso permite um código simples, mas ainda assim eficiente.

No próximo artigo, falarei sobre algumas otimizações permitidas pela programação funcional ao seu tempo de execução a fim de criar e mostrar exemplos no Groovy de memorização.

***

Ortogonalidade: A definição de ortogonal engloba diversas disciplinas, incluindo matemática e ciência da computação. Em matemática, dois vetores que são perpendiculares são ortogonais, ou seja, eles nunca realizam uma intersecção. Em ciência da computação, componentes ortogonais não têm qualquer efeito (ou efeito colateral) um sobre o outro. Por exemplo, a programação funcional e a metaprogramação são ortogonais no Groovy, pois não se interferem: o uso da metaprogramação não o impede de usar desenvolvimentos funcionais e vice-versa. O fato de serem ortogonais não significa que não podem trabalhar juntas, simplesmente porque elas não interferem uma na outra.

Inicializando os métodos de metaprogramação: É preciso adicionar métodos de metaprogramação antes da primeira tentativa de invocá-los. O local mais seguro para inicializá-los é no inicializador estático para a classe que os usa (pois é garantido que executará antes de outros inicializadores para a classe), mas isso adiciona complexidade quando diversas classes precisam de métodos aumentados. Geralmente, os aplicativos que usam muita metaprogramação acabam com uma classe de autoinicialização a fim de garantir que os inicializadores ocorram no tempo apropriado.

***

A solução IBM Rational para Collaborative Lifecycle Management combina Rational Team Concert, Rational Quality Manager e Rational Requirements Composer em uma única imagem, disponível no IBM SmartCloud Enterprise. Esta imagem de solução integrada pode ajudar a equipe do departamento de software a melhorar sua produtividade com recursos integrados de application lifecycle management (ALM).

Recursos

Aprender

Obter produtos e tecnologias

  • Functional Java: faça o download da estrutura de Functional Java.
  • Avalie os produtos IBM da maneira que for melhor para você: faça download da versão de teste de um produto, avalie um produto on-line, use-o em um ambiente de nuvem ou passe algumas horas na SOA Sandbox aprendendo a implementar Arquitetura Orientada a Serviços de modo eficiente.

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.

***

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