No primeiro artigo desta série, comecei a analisar algumas das características da programação funcional, mostrando como essas ideias se manifestam em Java e em linguagens mais funcionais. Neste artigo, continuarei essa apresentação de conceitos ao falar sobre as funções de primeira classe, otimizações e encerramentos. Mas o tema subjacente deste artigo é controle: quando o desejamos, quando precisamos dele e quando abrir mão dele.
Funções e controle de primeira classe
Usando a biblioteca Functional Java (veja os Recursos), mostrei, por último, a implementação de um classificador de número com os métodos funcionais isFactor() e factorsOf() , como mostrado na Listagem 1:
Listagem 1. Versão funcional do classificador de número
public class FNumberClassifier {
public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
public int sum(List<Integer> factors) {
return factors.foldLeft(fj.function.Integers.add, 0);
}
public boolean isPerfect(int number) {
return sum(factorsOf(number)) - number == number;
}
public boolean isAbundant(int number) {
return sum(factorsOf(number)) - number > number;
}
public boolean isDeficiend(int number) {
return sum(factorsOf(number)) - number < number;
}
}
|
Nos métodos isFactor() e factorsOf()
, eu cedi o controle do algoritmo de looping para a estrutura. — Agora ela decide a melhor maneira de executar a iteração no intervalo de números. Se a estrutura (ou — no caso de ser escolhida uma linguagem funcional como Clojure ou linguagem Scala — ) puder otimizar a implementação subjacente, seu benefício será automático. Embora a princípio talvez hesitemos em desistir desse controle, note que isso segue uma tendência geral em linguagens de programação e tempos de execução: com o tempo, o desenvolvedor fica mais abstraído dos detalhes que a plataforma pode manipular de forma mais eficiente. Nunca me preocupo com o gerenciamento de memória em JVM porque a plataforma permite que eu esqueça isso. Claro que, às vezes, isso torna algumas coisas mais difíceis, mas é uma boa troca em vista do benefício obtido em codificações diárias. Construções de linguagem funcional, como funções de ordem mais alta e de primeira classe, permitem subir mais um degrau na escada de abstração e focar mais em o que o código faz, em vez de em como faz isso.
Até mesmo com a estrutura de Functional Java, a codificação nesse estilo em Java é complicada, porque a linguagem na verdade não tem sintaxe nem construções para isso. Como fica a codificação funcional em uma linguagem que dispõe desses?
Clojure é um Lisp funcional projetado para JVM (veja os Recursos). Analise o classificador de número escrito em Clojure, mostrado na Listagem 2:
Listagem 2. Implementação em Clojure do classificador de número
(ns nealford.perfectnumbers)
(use '[clojure.contrib.import-static :only (import-static)])
(import-static java.lang.Math sqrt)
(defn is-factor? [factor number]
(= 0 (rem number factor)))
(defn factors [number]
(set (for [n (range 1 (inc number)) :when (is-factor? n number)] n)))
(defn sum-factors [number]
(reduce + (factors number)))
(defn perfect? [number]
(= number (- (sum-factors number) number)))
(defn abundant? [number]
(< number (- (sum-factors number) number)))
(defn deficient? [number]
(> number (- (sum-factors number) number)))
|
A maior parte do código da Listagem 2 é muito fácil de acompanhar, mesmo quando a pessoa não é um obstinado desenvolvedor Lisp — especialmente se aprendermos a ler de dentro para fora. Por exemplo, o método is-factor? pega dois parâmetros e pergunta se o resto é igual a 0 quando number é multiplicado por factor. De forma semelhante, os métodos perfect?, abundant? e deficient? devem ser fáceis de decifrar, principalmente se consultarmos a implementação em Java da Listagem 1.
O método sum-factors usa o método integrado reduce . sum-factors reduz a lista ao diminuir um elemento por vez, usando a função (nesse caso, +) fornecida como primeiro parâmetro em cada elemento. O método reduce aparece em diferentes formas em várias linguagens e estruturas. Ele apareceu na versão de Functional Java da Listagem 1 como método foldLeft() . O método factors retorna uma lista de números, por isso, estou processando uma lista por vez, adicionando cada elemento à soma acumulada, que é o valor de retorno de reduce. Nota-se que, depois de nos acostumarmos a pensar em termos de funções de ordem mais alta e de primeira classe, podemos reduzir (trocadilho intencional) muito do ruído no código.
O método factors pode parecer um conjunto aleatório de símbolos. Mas ele faz sentido depois de vermos as compreensões de lista, um dos vários recursos eficientes de manipulação de lista em Clojure. Como antes, é mais fácil entender factors de dentro para fora. Não se confunda com as diferentes terminologias de linguagens. A palavra-chave for em Clojure não significa um loop for . Em vez disso, pense nela como a avó de todas as construções de filtragem e transformação. Nesse caso, estou pedindo para ela filtrar o intervalo de números de 1 a number + 1), usando o predicado is-factor? (que é o método is-factor que defini antes na Listagem 2 — note o grande uso de funções de primeira classe), retornando os números correspondentes. O retorno dessa operação é uma lista de números que atendem os meus critérios de filtro, que eu imponho a um conjunto para remover as duplicatas.
Embora aprender uma nova linguagem dê muito trabalho, o esforço traz grandes benefícios no caso das linguagens funcionais quando entendemos seus recursos.
Um dos benefícios da mudança para um estilo funcional é a capacidade de aproveitar o suporte da função de ordem mais alta fornecido pela linguagem ou estrutura. Mas o que acontece naqueles momentos em que não desejamos desistir do controle? No meu exemplo anterior, comparei o comportamento interno dos mecanismos de iteração ao funcionamento interno do gerenciador de memória: na maioria das vezes ficamos felizes em não nos preocupar com esses detalhes. Mas às vezes nos preocupamos com eles, como no caso de otimizações e ajustes similares.
Nas duas versões Java do classificador de número que mostrei em "Pensando funcionalmente, Parte 1," otimizei o código que determina os fatores. A implementação nativa original usava o operador de módulo (%), que é totalmente ineficiente, para verificar todos os números de 2 até o próprio número de destino para determinar se ele é um fator. Pode-se otimizar o algoritmo notando que os fatores vêm em pares. Por exemplo, se estivermos procurando os fatores de 28 e encontrarmos 2, também será possível pegar 14. Se for possível obter os fatores em pares, bastará verificar os fatores até a raiz quadrada do número de destino.
A otimização, que era fácil de fazer na versão Java, parece impossível na versão Functional Java, porque não controlo a implementação do mecanismo de iteração de forma direta. Porém, parte do aprendizado de pensar funcionalmente exige a renúncia das noções sobre esse tipo de controle, com o objetivo de permitir que você exerça outro tipo de controle.
Posso expressar novamente o problema original de forma funcional: filtro todos os fatores de 1 ao number, retendo apenas os fatores que correspondem ao meu predicado isFactor() . Isso é implementado na Listagem 3:
Listagem 3. Método
isFactor() .
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
|
Embora seja elegante do ponto de vista declarativo, o código da Listagem 3 é bastante ineficiente porque verifica cada número. Depois de entender a otimização (coletando fatores em pares, apenas até a raiz quadrada), posso reformular o problema desta maneira:
- Filtrar todos os fatores do número de destino de 1 até a raiz quadrada do número.
- Dividir o número de destino por cada um desses fatores para obter o fator de simetria, e adicioná-lo à lista de fatores.
Com esse objetivo definido, posso escrever a versão otimizada do método factorsOf() usando a biblioteca de Functional Java, como mostrado na Listagem 4:
Listagem 4. Método otimizado de localização de fatores
public List<Integer> factorsOfOptimzied(final int number) {
List<Integer> factors =
range(1, (int) round(sqrt(number)+1))
.filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}});
return factors.append(factors.map(new F<Integer, Integer>() {
public Integer f(final Integer i) {
return number / i;
}}))
.nub();
}
|
O código na Listagem 4 se baseia no algoritmo que mencionei anteriormente, com uma sintaxe meio estranha que é exigida pela estrutura de Functional Java. Primeiro, pego o intervalo de números de 1 até a raiz quadrada do número de destino mais 1 (para garantir que eu obtenha todos os fatores). Segundo, eu filtro os resultados com base no uso do operador de módulo como nas versões anteriores, envolvido em um bloco de código de Functional Java. Salvo essa lista filtrada na variável factors . Na quarta etapa (lendo de dentro para fora), pego essa lista de fatores e executo a função map() , o que produz uma nova lista pela execução do meu bloco de código em cada elemento (mapping em cada elemento até um novo valor). Minha lista de fatores contém todos os fatores do meu número de destino até a sua raiz quadrada. Eu preciso dividir cada um pelo número de destino para obter seu fator de simetria, que é o que faz o bloco de código enviado para o método map() . Na quinta etapa, agora que tenho a lista de fatores de simetria, eu a anexo à lista original. Como última etapa, preciso considerar o fato de que estou mantendo os fatores em uma List em vez de em um Set. List são métodos convenientes para esses tipos de manipulações, mas um efeito colateral do meu algoritmo é uma entrada duplicada quando aparece uma raiz quadrada de número inteiro. Por exemplo, se o número de destino é 16, a raiz de número inteiro de 4 acabaria incluída duas vezes na lista de fatores. Para continuar a usar os convenientes métodos List , basta chamar seu método nub() no fim, o que remove todas as duplicatas.
Só porque normalmente renunciamos o conhecimento dos detalhes de implementação ao usar abstrações de nível superior, como a programação funcional, isso não significa que não podemos utilizar as de baixo nível, se necessário. Geralmente, a plataforma Java possui um isolamento contra elementos de baixo nível, mas se formos determinados, podemos obter o nível que necessitamos. Da mesma forma, em construções de programação funcional, geralmente cedemos voluntariamente os detalhes para a abstração, exceto em momentos em que isso realmente importa.
O que mais se destaca visualmente em todo o código Functional Java que mostrei até agora é a sintaxe do bloco, que usa classes genéricas e internas anônimas como uma espécie de construção de pseudobloco de código e tipo de encerramento. Encerramentos são um dos recursos comuns das linguagens funcionais. O que os torna tão úteis nesse setor?
Por que os encerramentos são especiais?
Um encerramento é uma função que carrega uma ligação implícita com todas as variáveis referenciadas dentro dela. Em outras palavras, a função (ou método) abrange um contexto em torno dos elementos que são referenciados por ela. Encerramentos são muito usados como mecanismo de execução portátil em linguagens e estruturas funcionais, passadas para funções de ordem mais alta, como map() , e código de transformação. O Functional Java usa classes internas anônimas para imitar parte do comportamento de encerramento "real", mas eles não podem ir até o fim porque Java não dispõe de suporte para encerramentos. Mas o que isso significa?
A Listagem 5 mostra um exemplo do que torna os encerramentos tão especiais. Ela é escrita em Groovy, que suporta encerramentos por meio de seu mecanismo de bloco de código.
Listagem 5. Código Groovy que ilustra encerramentos
def makeCounter() {
def very_local_variable = 0
return { return very_local_variable += 1 }
}
c1 = makeCounter()
c1()
c1()
c1()
c2 = makeCounter()
println "C1 = ${c1()}, C2 = ${c2()}"
// output: C1 = 4, C2 = 1
|
O método makeCounter() primeiro define uma variável local com um nome apropriado e, em seguida, retorna um bloco de código que usa essa variável. Note que o tipo de retorno para o método makeCounter() é um bloco de código, não um valor. Esse bloco de código não faz nada além de incrementar o valor da variável local e devolvê-lo. Coloquei chamadas return explícitas nesse código, ambas opcionais em Groovy, mas o código fica ainda mais complexo sem elas!
Para exercitar o método makeCounter() , designei o bloco de código a uma variável C1 , depois o chamei três vezes. Estou usando a parte sintática de Groovy para executar um bloco de código, que deve colocar um conjunto de parênteses ao lado da variável do bloco de código. A seguir, chamo makeCounter() novamente, designando uma nova instância do bloco de código a C2. Por último, executo C1 novamente junto com C2. Observe que cada um dos blocos de código acompanhou uma instância separada de very_local_variable. Foi isso o que quis dizer com abranger o contexto. Mesmo que uma variável local seja definida dentro do método, o bloco de código está vinculado a essa variável porque faz referência a ela, o que significa que deve acompanhá-la enquanto a instância do bloco de código estiver ativa.
O mais próximo que se pode chegar do mesmo comportamento em Java aparece na Listagem 6:
Listagem 6.
MakeCounter em Java
public class Counter {
private int varField;
public Counter(int var) {
varField = var;
}
public static Counter makeCounter() {
return new Counter(0);
}
public int execute() {
return ++varField;
}
}
|
São possíveis diversas variantes da classe Counter , mas ainda estamos presos ao gerenciamento do estado. Isso ilustra por que o uso de encerramentos exemplifica o pensamento funcional: permitir que o tempo de execução gerencie o estado. Em vez de forçá-lo a lidar com a criação de campos e o estado inicial (incluindo a perspectiva terrível de utilizar seu código em um ambiente multiencadeado), permita que a linguagem ou a estrutura gerencie esse estado de forma invisível.
Por fim, vamos obter encerramentos em alguma versão futura de Java (uma discussão sobre que felizmente está fora do escopo deste artigo). Sua aparição em Java terá dois benefícios excelentes. Primeiro, simplificará muito os recursos para os escritores de estruturas e bibliotecas, melhorando também a sintaxe. Segundo, fornecerá um denominador comum de baixo nível para suporte a encerramento em todas as linguagens executadas em JVM. Embora muitas linguagens de JVM suportem o encerramento, todas elas devem implementar suas próprias versões, o que torna complicado passar encerramentos entre as linguagens. Se a linguagem Java definisse um formato único, todas as outras linguagens poderiam aproveitar isso.
Ceder seu controle sobre detalhes de baixo nível é uma tendência geral no desenvolvimento de software. Ficamos felizes em renunciar a responsabilidade pela coleta de lixo, gerenciamento de memória e diferenças de hardware. A programação funcional representa a próxima etapa para abstração: ceder detalhes mais simples, como iteração, simultaneidade e estado, ao tempo de execução, tanto quanto possível. Isso não significa que não é possível assumir o controle novamente, se necessário — mas é preciso querer fazer, pois isso não é imposto.
No próximo artigo continuarei a explorar construções de programação funcional em Java e parentes próximos apresentando currying e aplicativo de método parcial.
Aprender
- The Productive Programmer (Neal Ford, O'Reilly Media, 2008): O livro mais recente de Neal Ford expande vários tópicos desta série.
- Scala: é uma linguagem moderna e funcional em JVM.
- Clojure: é um Lisp moderno e funcional executado em JVM.
- Podcast: Stuart Halloway on Clojure: saiba mais sobre Clojure e descubra as duas principais razões pelas quais ele foi rapidamente adotado e está crescendo rapidamente em popularidade.
- Functional Java: é uma estrutura que acrescenta muitas construções de linguagem funcional a Java.
- "Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories" (Scott Davis, developerWorks, junho de 2009): leia sobre encerramentos em Groovy.
-
Navegue pela livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
-
Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.
Obter produtos e tecnologias
- Faça o download do Versões de avaliação de produtos IBM ou explore as versões de teste on-line no IBM SOA Sandbox e entre em contato com as ferramentas de desenvolvimento de aplicativos e produtos de middleware do DB2®, Lotus®, Rational®, Tivoli®eWebSphere®.
Discutir
-
Confira blogs do developerWorks e participe da comunidade do developerWorks.

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.