Back-End

20 dez, 2012

Pensamento funcional: grandes transformações

Publicidade

As linguagens de programação funcional abordam a reutilização de código de forma diferente das linguagens orientadas a objeto, um tópico que eu abordei em “Acoplamento e composição – Parte 02“. As linguagens orientadas a objetos tendem a ter muitas estruturas de dados com muitas operações, enquanto linguagens funcionais têm poucas estruturas de dados com muitas operações. Linguagens orientadas a objetos incentivam a criação de métodos específicos de classes, e é possível capturar padrões recorrentes para reutilização posterior. Linguagens funcionais ajudam a reutilizar incentivando a aplicação de transformações comuns nas estruturas de dados, com funções de maior ordem para customizar a operação de instâncias específicas.

As mesmas estruturas e operações de dados aparecem em todas as linguagens funcionais — e em muitas das estruturas e que têm suporte para programação funcional em Java — mas seus nomes muitas vezes são diferentes. A confusão na nomenclatura dificulta a passagem de conhecimento de uma linguagem para outra, mesmo que os conceitos subjacentes sejam os mesmos.

O objetivo desta parte do artigo é facilitar essa tradução. Ele começa com um simples problema que requer decisão e iteração e implementa a solução em cinco linguagens (Java, Groovy, Clojure, JRuby e Scala) e duas estruturas funcionais (Functional Java e Totally Lazy) para Java (consulte Recursos). A implementação é a mesma, mas os detalhes são um pouco diferentes entre as linguagens.

Java simples

O problema é determinar se um número inteiro é primo — um número cujos únicos fatores são 1 e ele mesmo. Existem vários algoritmos para determinar se um número é primo (algumas soluções alternativas aparecem em “Acoplamento e composição – Parte 01“). Eu usarei uma solução que determina os fatores do número e verifica se a soma dos fatores é igual ao número mais 1, o que indica que o número é primo. Esse não é o algoritmo mais eficiente, mas meu objetivo é mostrar diferentes implementações de métodos comuns de coleta e não discutir eficiência.

A versão Java simples aparece na Listagem 1:

public class PrimeNumberClassifier {
private Integer number;

public PrimeNumberClassifier(int number) {
this.number = number;
}

public boolean isFactor(int potential) {
return number % potential == 0;
}

public Set<Integer> getFactors() {
Set<Integer> factors = new HashSet<Integer>();
factors.add(1);
factors.add(number);
for (Integer i = 2; i < number; i++)
if (isFactor(i))
factors.add(i);
return factors;
}

public int sumFactors() {
int sum = 0;
for (int i : getFactors())
sum += i;
return sum;
}

public boolean isPrime() {
return sumFactors() == number + 1;
}
}

Se você leu partes anteriores do artigo pensamento funcional, reconhecerá o algoritmo encontrado no método getFactors() na Listagem 1. Seu núcleo é o método isFactor(), que classifica os fatores candidatos.

Groovy

Groovy incluiu muitos desenvolvimentos funcionais na sua evolução, o que levou a uma implementação, mostrada na Listagem 2, bem diferente da implementação em Java:

class PrimeNumberClassifier {
private int number;

PrimeNumberClassifier(int number) {
this.number = number
}

public def isFactor(int potential) {
number % potential == 0;
}

public def getFactors() {
(1..number).findAll { isFactor(it) }.toSet()
}

public def sumFactors() {
getFactors().inject(0, {i, j -> i + j})
}

public def isPrime() {
sumFactors() == number + 1
}
}

Dois métodos com contrapartes na Listagem 1 mudaram mais do que apenas a sintaxe na Listagem 2. O primeiro, getFactors(), usa a classe Range do Groovy para representar os números candidatos. O método findAll() aplica o bloco de códigos a cada elemento da coleção, retornando uma lista com os itens para os quais o bloco retorna true. O bloco de códigos aceita um único parâmetro, que é o elemento sendo investigado. Eu uso atalhos convenientes do Groovy para tornar o bloco mais conciso. Por exemplo, ele poderia ser escrito como (1..number).findAdd {i-> isFactor(i) }, mas a repetição do parâmetro único é redundante. O Groovy oferece a opção mostrada na Listagem 2 de substituir o parâmetro solitário com o implícito it.

Outro método digno de nota na Listagem 2 é o método sumFactors(). Usando o conjunto de números gerados pelo método getFactors(), eu chamo inject(), que é um método de coleção do Groovy que realiza uma operação fold. O método inject() combina cada elemento na coleção usando o bloco de códigos fornecido no segundo parâmetro, usando o primeiro parâmetro como o valor inicial. O parâmetro do bloco de códigos na Listagem 2 é {i, j-> i + j}, que retorna a soma de dois números. O método inject() aplica esse bloco (começando com o primeiro elemento) a cada elemento por vez, somando a lista de números.

O uso de métodos funcionais com funções de ordem maior pode às vezes gerar código mais denso. Embora cada método na Listagem 2 seja uma linha única, ainda é vantajoso partir cada um em métodos individuais. Separar os métodos por função e dar a cada um deles um nome com significado no contexto do problema atual torna o raciocínio mais fácil.

Scala

A versão em Scala do classificador de números primos está na Listagem 3:

object PrimeNumberClassifier {
def isFactor(number: Int, potentialFactor: Int) =
number % potentialFactor == 0

def factors(number: Int) =
(1 to number) filter (isFactor(number, _))

def sum(factors: Seq[Int]) =
factors.foldLeft(0)(_ + _)

def isPrime(number: Int) =
sum(factors(number)) == number + 1
}

Além de ser muito mais curta, a versão em Scala têm outras diferenças. Como precisarei de uma única instância, eu faço dela um object em vez de class. O método factors() usa a mesma implementação que a versão em Groovy na Listagem 2, mas com sintaxe diferente. Eu aplico o método filter (a versão em Scala do findAll() do Groovy) ao intervalo de número (1 to number), usando o método isFactor() definido no começo da Listagem 3 como predicado. Scala também permite marcadores de parâmetro — o _ neste caso.

O método sum() na Listagem 3 usa o método foldLeft() do Scala, que é sinônimo do inject() do Groovy. Nesse caso, eu uso zero como valor inicial e marcadores para ambos os parâmetros.

Clojure

Clojure é uma implementação moderna de Lisp na JVM, o que leva à sintaxe muito diferente da Listagem 4:

(ns prime)

(defn factor? [n, potential]
(zero? (rem n potential)))

(defn factors [n]
(filter #(factor? n %) (range 1 (+ n 1))))

(defn sum-factors [n]
(reduce + (factors n)))

(defn prime? [n]
(= (inc n) (sum-factors n)))

Todos os métodos na Listagem 4 são estranhos para desenvolvedores Java, mas o código implementa o mesmo algoritmo que eu estive usando o tempo todo. O método (factor?) verifica se o resto (a função rem no Clojure) é zero. O método (factors) usa o método (filter) do Clojure, que aceita dois parâmetros. O primeiro parâmetro é o bloco de códigos predicado a ser executado em cada elemento, que espera um resultado de operador booleano sobre se ele passa pelos critérios do filtro. A sintaxe de #(factor? n %) representa uma função anônima do Clojure, usando o parâmetro de substituição % do Clojure. O segundo parâmetro da função (filter) é a coleção a ser filtrada, que, neste caso, é o intervalo de 1 até o número de destino mais 1. Os intervalos excluem o último elemento.

O método (sum-factors) na Listagem 4 usa o método (reduce) do Clojure, um sinônimo de inject() do Groovy e foldLeft() do Scala. Nesse caso, a operação é o operador simples +, que, para o Clojure, é indistinguível de qualquer outro método que aceita dois parâmetros e retorna um resultado.

Embora a sintaxe possa ser desafiadora para quem não está acostumado, a versão do Clojure é muito concisa. Assim como na versão Groovy, bons nomes de funções são importantes, mesmo que cada função seja uma única linha, pois as linhas às vezes têm bastante conteúdo.

JRuby

JRuby é uma implementação de JVM do Ruby, que também ganhou muitos desenvolvimentos funcionais na sua vida. Considere a versão em (J)Ruby do classificador de números primos na Listagem 5:

class PrimeNumberClassifier
def initialize(num)
@num = num
end

def factor?(potential)
@num % potential == 0
end

def factors
(1..@num).select { |i| factor?(i) }
end

def sum_factors
factors.reduce(0, :+)
end

def prime?
(sum_factors == @num + 1)
end
end

O método factors na Listagem 5 inclui o sinônimo select para o findAll do Groovy e para o método filter do Scala e Clojure. Um dos bons recursos do JRuby é a capacidade de criar facilmente alias para métodos, proporcionando nomes mais convenientes para métodos em diferentes contextos. Obviamente, o JRuby inclui um alias para o método select chamado find_all, mas ele não é tão comum no uso idiomático.

Para o método sum_factors na Listagem 5, eu usei o método reduce, semelhante a várias outras linguagens. No JRuby, como no Clojure, os operadores são métodos com nomes curiosos. Ruby permite especificar o nome do método de adição por seu símbolo, :+. Para auxiliar na capacidade de leitura, Clojure e Ruby permitem incluir pontos de interrogação em funções que devem retornar operadores booleanos. E, sendo consistente com sua natureza, o Ruby inclui um alias do método inject para reduce.

Functional Java

Para não desamparar quem ainda usa variantes de Java, várias bibliotecas de programação funcional aumentam Java com desenvolvimentos funcionais. Functional Java é uma delas. O classificador de número primo usando Java mais Functional Java está na Listagem 6:

public class FjPrimeNumberClassifier {
private int number;

public FjPrimeNumberClassifier(int number) {
this.number = number;
}

public boolean isFactor(int potential) {
return number % potential == 0;
}

public List<Integer> getFactors() {
return range(1, number + 1)
.filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return isFactor(i);
}
});
}

public int sumFactors() {
return getFactors().foldLeft(fj.function.Integers.add, 0);
}

public boolean isPrime() {
return sumFactors() == number + 1;
}
}

O método getFactors() na Listagem 6 usa o método filter() em range (também exclusivo, como no Clojure — por isso o número + 1 na definição do intervalo). Como Java ainda não tem funções de ordem maior, Functional Java trapaceia com instâncias de classe interna anônimas de sua classe F integrada, parametrizando os tipos usando genéricos.

Assim como Scala, Functional Java inclui um método foldLeft(), aceitando, nesse caso, um bloco de códigos predefinido que soma os números e o valor inicial.

Totally Lazy

Totally Lazy é uma biblioteca funcional para Java que inclui nessa linguagem algumas coleções preguiçosas. Estruturas de dados preguiçosas não predefinem os elementos, mas sim codificam as regras de como gerar o próximo valor quando ele for solicitado. O classificador de número primo implementado em Totally Lazy está na Listagem 7

public class TlPrimeNumberClassifier {
private int number;

public TlPrimeNumberClassifier(int number) {
this.number = number;
}

public boolean isFactor(Integer potential) {
return (number % potential) == 0;
}

private List<Integer> listOfPotentialFactors() {
List<Integer> results = new ArrayList<Integer>();
for (int i = 1; i <= number + 1; i++)
results.add(i);
return results;
}

public boolean isPrime() {
return (this.number + 1) ==
Sequences.init(listOfPotentialFactors())
.filter(new Predicate<Integer>() {
@Override
public boolean matches(Integer other) {
return isFactor(other);
}})
.foldLeft(0, sum())
.intValue();
}
}

O método isPrime() na Listagem 7 usa a classe Sequences, inicializada com uma lista de todos os fatores potenciais (ou seja, todos os números de 1 ao número de destino) e, em seguida, aplica o método filter(). No Totally Lazy, o método filter() espera uma subclasse da classe Predicate, muitas das quais já estão implementadas para casos comuns. No meu exemplo, eu substituí o método matches() pelo meu próprio método isFactor() para determinar a inclusão. Quando tiver a lista de fatores, eu uso o método foldLeft, usando o método fornecido sum() como operação de compactação.

No exemplo mostrado na Listagem 7, o método isPrime() faz a maior parte do trabalho pesado. O fato de que todas as estruturas de dados no Totally Lazy são preguiçosas às vezes traz complicações ao combiná-las. Considere a versão do método getFactors(), mostrado na Listagem 8:

public Iterator<Integer> getFactors() {
    return Sequences
            .init(listOfPotentialFactors())
            .filter(new Predicate<Integer>() {
                @Override
                public boolean matches(Integer other) {
                    return isFactor(other);
                }
            })
            .iterator();
}

O tipo de retorno do método getFactors() na Listagem 8 é Iterator<Integer> — mas é um agente iterativo preguiçoso, o que significa que a coleção não terá valores até que seja feita uma iteração dela. Isso faz das coleções preguiçosas um desafio em termos de teste. Considere o teste de unidade para o exemplo do Totally Lazy da Listagem 8, mostrado na Listagem 9:

@Test
public void factors() {
TlPrimeNumberClassifier pnc = new TlPrimeNumberClassifier(6);
List<Integer> expected = new ArrayList<Integer>() {{
add(1);
add(2);
add(3);
add(6);
}};
Iterator<Integer> actual = pnc.getFactors();
assertTrue(actual.hasNext());
int count = 0;
for (int i = 0; actual.hasNext(); i++) {
assertEquals(expected.get(i), actual.next());
count++;
}
assertTrue(count == expected.size());
}

Para uma coleção preguiçosa, é necessário passar pela coleção para recuperar os valores e certificar-se de que não haja mais elementos na lista preguiçosa do que o esperado.

Embora seja possível criar uma versão em Totally Lazy que seja tão bem fatorada como as demais, podemos acabar tendo de lidar com estruturas de dados esdrúxulas como <Iterator<Iterator<Number>>>.

Conclusão

Nesta parte do artigo, eu desmistifiquei a proliferação de nomes para o mesmo comportamento em diversas linguagens funcionais e estruturas. Os nomes dos métodos nas diferentes linguagens e estruturas nunca serão os mesmos, mas, à medida que o tempo de execução Java inclui desenvolvimentos como encerramento, a interoperabilidade irá tornar-se mais fácil, pois podem compartilhar representações padrão comum (em vez de precisar, como no caso de Totally Lazy, de desenvolvimentos estranhos como <Iterator<Iterator<Number>>>).

Na próxima parte do artigo, continuarei investigando semelhanças em transformações ao examinar mapas.

***

O IBM® Rational® Team Concert, baseado na plataforma Jazz, agora suporta qualquer plano, qualquer processo, qualquer plataforma! Novos modelos de planejamento formal suportam fases de projetos tradicionais, enquanto que novos recursos de gerenciamento de risco podem ser usados por qualquer equipe tradicional, agile ou híbrida. Você poderá combinar e corresponder implementações para atender a seus ambientes particulares com um único release para todas as plataformas, licenciamento de usuário simples com base em função, sem cobrança do software do servidor e com novos modelos flexíveis de precificação.

Recursos

Aprender

  • The Productive Programmer (Neal Ford, O’Reilly Media, 2008): o livro de Neal Ford discute as ferramentas e práticas que ajudam a melhorar sua codificação de maneira eficiente.
  • Scala: é uma linguagem moderna e funcional em JVM.
  • Clojure: Clojure é um Lisp moderno e funcional que é executado na JVM.
  • Totally Lazy: Totally Lazy é uma estrutura que inclui várias coleções preguiçosas em Java.
  • JRuby: JRuby é uma implementação do Ruby na JVM que oferece muitos desenvolvimentos funcionais.
  • Groovy: Groovy é uma linguagem dinâmica semelhante a Java que contém muitas extensões funcionais da sintaxe Java.
  • Functional Java: é uma estrutura que acrescenta muitos desenvolvimentos de linguagem funcional a Java.
  • Navegue pela livraria de tecnologia para ver livros sobre este e outros assuntos técnicos.
  • Área de tecnologia developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.

Discutir

Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.

***

Sobre o autor: Neal Ford é arquiteto de software e Meme Wrangler na ThoughtWorks, uma consultoria global de TI. Ele também 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 várias tecnologias, incluindo o mais recente, The Productive Programmer. Sua especialidade é o projeto e a criação de aplicativos corporativos de grande porte. Também é palestrante admirado em conferências de desenvolvedores no mundo todo. Conheça seu website website.

***

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