Avançar para a área de conteúdo

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

A primeira vez que acessar o developerWorks, um perfil será criado para você. Informações do seu perfil (tais como: nome, país / região, e empresa) estarão disponíveis ao público, que poderá acompanhar qualquer conteúdo que você publicar. Seu perfil no developerWorks pode ser atualizado a qualquer momento.

Todas as informações enviadas são seguras.

  • Fechar [x]

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

Todas as informações enviadas são seguras.

  • Fechar [x]

Pensamento Funcional: Pensando funcionalmente, Parte 2

Explorando a programação e o controle funcionais

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.
Photo of Neal Ford
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.

Resumo:  As linguagens e estruturas funcionais permitem que o tempo de execução controle detalhes simples de codificação, como iteração, simultaneidade e estado. Mas isso não significa que não seja possível retomar o controle quando necessário. Um aspecto importante do pensamento funcional é saber quanto controle conceder e quando.

Visualizar mais conteúdo nesta série

Data:  04/Jul/2011
Nível:  Intermediário Também disponível em :   Inglês
Atividade:  1823 visualizações
Comentários:  


Sobre esta série

Esta série visa reorientar sua perspectiva para uma mentalidade funcional, ajudando a encarar problemas comuns de maneiras novas e encontrar maneiras de melhorar sua codificação diária. Ela explora os conceitos de programação funcional, estruturas que permitem programação funcional dentro da linguagem Java ™ , linguagens de programação funcional executadas em JVM e algumas indicações de tendência futura do design da linguagem. A série é voltada para desenvolvedores que conhecem Java e como suas abstrações funcionam, mas têm pouca ou nenhuma experiência na utilização de uma linguagem funcional.

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?

Classificador em Clojure

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.

Otimização

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:

  1. Filtrar todos os fatores do número de destino de 1 até a raiz quadrada do número.
  2. 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.


Conclusão

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.


Recursos

Aprender

Obter produtos e tecnologias

Discutir

Sobre o autor

Photo of Neal Ford

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.

Ajuda para Relatar Abuso

Relatar abuso

Obrigado. Esta entrada foi sinalizada para atenção do moderador.


Ajuda para Relatar Abuso

Relatar abuso

Falha no envio do Relatório de abuso. Tente novamente mais tarde.


developerWorks: Registre-se


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

Selecione seu nome de exibição

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

(Deve possuir de 3 a 31 caracteres.)


Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Classificar este artigo

Comentários

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=697131
ArticleTitle=Pensamento Funcional: Pensando funcionalmente, Parte 2
publish-date=07042011
author1-email=nford@thoughtworks.com
author1-email-cc=jaloi@us.ibm.com