Pensamento Funcional: Pensando funcionalmente, Parte 1

Aprendendo a pensar como um programador funcional

Recentemente, houve um grande aumento no interesse pela programação funcional, com promessas de menos erros e maior produtividade. Contudo, muitos desenvolvedores tentaram, mas não conseguiram, entender o que torna as linguagens funcionais interessantes para certos tipos de trabalho. Aprender a sintaxe de uma nova linguagem é fácil, mas aprender a pensar de um modo diferente é difícil. Na primeira parte desta série de artigos sobre Pensamento funcional , Neal Ford apresenta alguns conceitos de programação funcional e explica como usá-los no Java™ e no Groovy.

Neal Ford, Application Architect, ThoughtWorks Inc.

Neal FordNeal 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.



26/Mai/2011

Sobre esta série

Esta série visa reorientar sua perspectiva para uma mentalidade funcional, ajudando-o a identificar problemas comuns de novas formas e a encontrar modos de melhorar sua codificação no dia a dia. Ela explora conceitos de programação funcional, estruturas que possibilitam a programação funcional dentro da linguagem Java, linguagens de programação funcional que executam na JVM e algumas orientações para a aprendizagem futura do design de linguagens. Esta série é voltada para desenvolvedores que conhecem Java e sabem como suas abstrações funcionam, mas têm pouca ou nenhuma experiência com o uso de uma linguagem funcional.

Vamos supor, por um momento, que você é um lenhador. Você tem o melhor machado da floresta, o que faz de você o lenhador mais produtivo do acampamento. Então, um dia, aparece alguém elogiando as virtudes de um novo paradigma de derrubada de árvores — a motosserra. O vendedor é persuasivo, portanto, você compra uma motosserra, mas não sabe como ela funciona. Você tenta levantá-la e bater na árvore fortemente com ela, de acordo com o outro paradigma de derrubada de árvores. Você conclui rapidamente que a motosserra é uma moda passageira e volta a derrubar árvores com o machado. Em seguida, alguém chega e ensina você a acionar a motosserra.

Provavelmente, você se identifica com essa história, mas com a programação funcional no lugar da motosserra. O problema de um paradigma de programação totalmente novo não é aprender uma nova linguagem. Afinal de contas, a sintaxe da linguagem é só um detalhe. A parte difícil é aprender a pensar de forma diferente. É neste ponto em que eu entro — como acionador de motosserra e programador funcional.

Bem-vindo ao Pensamento funcional. Esta série explora o assunto da programação funcional, mas não trata somente das linguagens de programação funcionais. Conforme eu ilustrarei, escrever o código de forma "funcional" tem a ver com o design, compensações, diferentes blocos de construção reutilizáveis e vários outros insights. Na medida do possível, tentarei mostrar conceitos de programação funcional em Java (ou linguagens semelhantes a Java) e passar para outras linguagens para demonstrar recursos que ainda não existem nessa linguagem. Não vou começar pela parte mais difícil e falar de coisas sofisticadas como mônadas (consulte Recursos) logo de início (mas chegaremos lá). Em vez disso, mostrarei gradualmente uma nova forma de pensar sobre os problemas (que você já está aplicando a alguns pontos — só não percebeu isso ainda).

Esta parte e a próxima servem como um tour rápido por alguns assuntos relacionados à programação funcional, incluindo os conceitos centrais. Alguns desses conceitos voltarão com mais detalhes à medida que eu contextualizo e apresento novas facetas ao longo desta série. Como ponto de partida para o tour, apresentarei a você duas implementações diferentes de um problema, uma escrita de forma imperativa e outra com um toque mais funcional.

Classificador de número

Para falar de dois estilos de programação diferentes, é necessário ter códigos para comparação. Meu primeiro exemplo é a uma variação de um problema de codificação que aparece no meu livro The Productive Programmer (consulte Recursos) e em "Design Movido a Testes, Parte 1" e "Design Movido a Testes, Parte 2" (duas partes de minha série anterior do developerWorks Evolutionary architecture and emergent design). Escolhi esse código, pelo menos em parte, porque esses dois artigos descrevem o design do código em profundidade. Não há nada de errado com o design elogiado nesses artigos, mas eu apresentarei aqui uma justificativa para um design diferente.

Os requisitos estabelecem que, dado um número inteiro positivo maior que 1, você deve classificá-lo como perfeito, abundante ou deficiente. Um número perfeito é aquele cuja soma dos fatores (excluindo o próprio número como fator) é igual ao número. Da mesma forma, a soma dos fatores de um número abundante é maior que o número, em um número deficiente, essa soma é menor.

Classificador imperativo de números

Uma classe imperativa que preenche esses requisitos aparece na Listagem 1:

Listagem 1. NumberClassifier, a solução imperativa para o problema
public class Classifier6 {
    private Set<Integer> _factors;
    private int _number;

    public Classifier6(int number) {
        if (number < 1)
            throw new InvalidNumberException(
            "Can't classify negative numbers");
        _number = number;
        _factors = new HashSet<Integer>>();
        _factors.add(1);
        _factors.add(_number);
    }

    private boolean isFactor(int factor) {
        return _number % factor == 0;
    }

    public Set<Integer> getFactors() {
        return _factors;
    }

    private void calculateFactors() {
        for (int i = 1; i <= sqrt(_number) + 1; i++)
            if (isFactor(i))

                addFactor(i);
    }

    private void addFactor(int factor) {
        _factors.add(factor);
        _factors.add(_number / factor);
    }

    private int sumOfFactors() {
        calculateFactors();
        int sum = 0;
        for (int i : _factors)
            sum += i;
        return sum;
    }

    public boolean isPerfect() {
        return sumOfFactors() - _number == _number;
    }

    public boolean isAbundant() {
        return sumOfFactors() - _number > _number;
    }

    public boolean isDeficient() {
        return sumOfFactors() - _number < _number;
    }

    public static boolean isPerfect(int number) {
        return new Classifier6(number).isPerfect();
    }
}

Vários itens nesse código devem ser ressaltados:

  • Ele possui testes extensos de unidades (em parte, porque eu o escrevi para uma explicação sobre o desenvolvimento baseado em testes).
  • A classe consiste em um grande número de métodos coesos, um efeito colateral do uso do desenvolvimento baseado em teste nessa construção.
  • Uma otimização de desempenho está integrada ao método calculateFactors() . A parte principal dessa classe consiste na coleta de fatores para que eu possa somá-los e, no final, classificá-los. Os fatores sempre podem ser coletados em pares. Por exemplo, se o número em questão é 16, quando eu pego o fator 2, também posso pegar o 8, porque 2 x 8 = 16. Se eu coleto fatores em pares, só preciso procurar fatores até a raiz quadrada do número de destino, precisamente o que o calculateFactors() faz.

Classificador (ligeiramente mais) funcional

Usando as mesmas técnicas de desenvolvimento baseado em teste, eu criei uma versão alternativa do classificador, que aparece na Listagem 2:

Listagem 2. Classificador de números ligeiramente mais funcional
public class NumberClassifier {

    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set<Integer> factors(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(number, i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }

    static public int sum(Set<Integer> factors) {
        Iterator it = factors.iterator();
        int sum = 0;
        while (it.hasNext())
            sum += (Integer) it.next();
        return sum;
    }

    static public boolean isPerfect(int number) {
        return sum(factors(number)) - number == number;
    }

    static public boolean isAbundant(int number) {
        return sum(factors(number)) - number > number;
    }

    static public boolean isDeficient(int number) {
        return sum(factors(number)) - number < number;
    }
}

As diferenças entre essas duas versões do classificador são sutis, mas importantes. A principal diferença é a falta proposital do estado compartilhado na Listagem 2. A eliminação (ou pelo menos a diminuição) do estado compartilhado é uma das abstrações mais favorecidas na programação funcional. Em vez de compartilhar o estado entre os métodos como resultados intermediários (consulte o campo factors na Listagem 1), eu chamo os métodos diretamente, eliminando o estado. Do ponto de vista do design, ele deixa o método factors() mais longo, mas impede que o campo factors "vaze" do método. Observe também que a versão da Listagem 2 poderia ser formada inteiramente por métodos estáticos. Não há conhecimento compartilhado entre os métodos, portanto, eu tenho menos necessidade de encapsulamento por meio de escopo. Todos esses métodos funcionam perfeitamente bem se você dá a eles os tipos de parâmetros de entrada que eles esperam. (Este é um exemplo de função pura, um conceito que abordarei mais detalhadamente em uma parte futura).


Funções

A programação funcional é uma área ampla e em expansão da ciência da computação, na qual houve recentemente um grande aumento de interesse. Novas linguagens funcionais na JVM (como Scala e Clojure) e estruturas (como o Functional Java e Akka), estão em evidência (consulte Recursos), juntamente com as alegações usuais de menos erros, mais produtividade, melhor aparência, mais dinheiro, etc. Em vez de tentar abordar todo o assunto de programação funcional logo de início, eu me concentrarei em vários conceitos chave e seguirei algumas implicações interessantes derivadas desses conceitos.

O aspecto central da programação funcional é a função, da mesma forma que as classes são a principal abstração das linguagens orientadas a objeto. As funções são os elementos de desenvolvimento do processamento e possuem vários recursos que não são encontrados nas linguagens imperativas tradicionais.

Funções de alta ordem

As funções de alta ordem podem tomar funções como argumentos ou retorná-las como resultados. Não temos essa construção na linguagem Java. O mais próximo dessa construção em Java é o uso de uma classe (frequentemente uma classe anônima) como "portadora" de um método que você precisa executar. O Java não tem funções autônomas (ou métodos autônomos), portanto, as funções não podem ser retornadas de funções nem passadas como parâmetros.

Esse recurso é importante em linguagens funcionais por pelo menos duas razões. Primeiro, o fato de ter funções de alta ordem significa que é possível pressupor como as partes da linguagem irão se encaixar. Por exemplo, é possível eliminar categorias inteiras de métodos em uma hierarquia de classes desenvolvendo um mecanismo geral que atravesse listas e aplique uma função de alta ordem (ou mais) a cada elemento. (Mostrarei um exemplo dessa construção logo mais). Em segundo lugar, ao habilitar funções como valores de retorno, você cria a oportunidade de desenvolver sistemas altamente dinâmicos e adaptáveis.

Os problemas que podem ser resolvidos por funções de alta ordem não são exclusivos das linguagens funcionais. Entretanto, a forma de resolver o problema é diferente quando você pensa de modo funcional. Considere o exemplo da Listagem 3 (tomado de uma base de código maior) de um método que realiza o acesso a dados protegidos:

Listagem 3. Modelo de código que pode ser reutilizado
public void addOrderFrom(ShoppingCart cart, String userName,
                     Order order) throws Exception {
    setupDataInfrastructure();
    try {
        add(order, userKeyBasedOn(userName));
        addLineItemsFrom(cart, order.getOrderKey());
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

O código na Listagem 3 faz a inicialização, realiza algumas tarefas, conclui a transação, se foi bem-sucedida, retrocede se algo deu errado e, por fim, limpa os recursos. Claramente, o estereótipo desse código pode ser reutilizado, o que é normalmente feito nas linguagens orientadas a objeto, através da criação de uma estrutura. Nesse caso, combinarei dois dos padrões de design da Gang of Four (consulte Recursos): o método de modelo e os padrões de comando. O padrão do método de modelo sugere que eu devo mover o código estereotipado comum para cima na hierarquia de herança, adiando os detalhes algorítmicos para as classes filho. O padrão de design de comando fornece uma forma de encapsular o comportamento em uma classe com uma semântica de execução bem conhecida. A Listagem 4 mostra o resultado da aplicação desses dois padrões ao código da Listagem 3:

Listagem 4. Código com a ordem refatorada
public void wrapInTransaction(Command c) throws Exception {
    setupDataInfrastructure();
    try {
        c.execute();
        completeTransaction();
    } catch (Exception condition) {
        rollbackTransaction();
        throw condition;
    } finally {
        cleanUp();
    }
}

public void addOrderFrom(final ShoppingCart cart, final String userName,
                         final Order order) throws Exception {
    wrapInTransaction(new Command() {
        public void execute() {
            add(order, userKeyBasedOn(userName));
            addLineItemsFrom(cart, order.getOrderKey());
        }
    });                
}

Na Listagem 4, eu extraio as partes genéricas do código para o método wrapInTransaction() (cuja semântica você talvez reconheça — é basicamente uma versão simples do TransactionTemplate do Spring), que passa um objeto Command como unidade de trabalho. O método addOrderFrom() é ocultado na definição da criação de uma classe interna anônima da classe de comando, empacotando os dois itens de trabalho.

O empacotamento do comportamento necessário em uma classe de comando é puramente um artefato do design de Java, que não inclui nenhum tipo de comportamento autônomo. Em Java, todo comportamento deve residir dentro de uma classe. Até os designers da linguagem viram rapidamente uma deficiência nesse design — em retrospecto, é um pouco ingênuo pensar que nunca haverá um comportamento que não esteja ligado a uma classe. O JDK 1.1 corrigiu essa deficiência incluindo classes internas anônimas, que, pelo menos, forneciam recursos sintáticos para a criação de várias classes pequenas com apenas alguns métodos puramente funcionais, não estruturais. Para ver um ensaio muito divertido e humorístico sobre esse aspecto do Java, confira "Execution in the Kingdom of Nouns" de Steve Yegge (consulte Recursos).

O Java me força a criar uma instância de uma classe Command , mas o que eu realmente quero é o método que está dentro da classe. A classe em si não oferece nenhum benefício: não possui campos, construtor (além do construtor gerado automaticamente a partir do Java), nem estado. Atua unicamente como um wrapper para o comportamento dentro do método. Em uma linguagem funcional, isso seria tratado por uma função de alta ordem.

Se eu estiver disposto a sair temporariamente da linguagem Java, poderei me aproximar semanticamente do ideal da programação funcional usando fechamentos. A Listagem 5 mostra o mesmo exemplo refatorado, mas usando o Groovy (consulte Recursos) em vez de Java:

Listagem 5. Usando fechamentos do Groovy em vez de classes de comando
def wrapInTransaction(command) {
  setupDataInfrastructure()
  try {
    command()
    completeTransaction()
  } catch (Exception ex) {
    rollbackTransaction()
    throw ex
  } finally {
    cleanUp()
  }
}

def addOrderFrom(cart, userName, order) {
  wrapInTransaction {
    add order, userKeyBasedOn(userName)
    addLineItemsFrom cart, order.getOrderKey()
  }
}

No Groovy, tudo o que está entre chaves {} é um bloco de código, e blocos de código podem ser passados como parâmetros, imitando uma função de alta ordem. Nos bastidores, o Groovy está implementando o padrão de design de comando para você. Cada bloco de fechamento no Groovy é, na verdade, uma instância de um tipo de fechamento do Groovy, que inclui um método call() que é chamado automaticamente quando você coloca um conjunto vazio de parênteses depois da variável que contém a instância de fechamento. O Groovy possibilitou alguns comportamentos semelhantes aos da programação funcional desenvolvendo as estruturas de dados apropriadas, com os recursos sintáticos correspondentes, na linguagem propriamente dita. Como será mostrado em partes futuras, o Groovy também inclui outros recursos de programação funcional que vão além do Java. Também voltarei a fazer algumas comparações interessantes entre fechamentos e funções de alta ordem em uma parte futura.

Funções de primeira classe

Em uma linguagem funcional, as funções são consideradas de primeira classe, ou seja, podem aparecer em qualquer lugar, assim como qualquer outra construção da linguagem (como as variáveis). A presença das funções de primeira classe permite o uso de funções de formas inesperadas e obriga a pensar nas soluções de forma diferente, por exemplo, aplicar operações relativamente genéricas (com detalhes sutis) a estruturas de dados padrão. Isso, por sua vez, expõe uma mudança fundamental no pensamento que ocorre nas linguagens funcionais: Foco nos resultados, não nas etapas.

Nas linguagens de programação imperativas, é necessário pensar em cada etapa individual no algoritmo. O código na Listagem 1 mostra isso. Para resolver o classificador de números, eu tive que discernir o modo exato de coletar os fatores, o que, por sua vez, significa que foi necessário escrever um código específico para executar loops nos números, a fim de determinar os fatores. Entretanto, executar loops em listas e realizar operações em cada elemento parecem coisas muito comuns. Considere o código reimplementado de classificação de números, que usa a estrutura funcional do Java, mostrado na Listagem 6:

Listagem 6. Classificador funcional de números
public class FNumberClassifier {

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

    public List<Integer> factors(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(factors(number)) - number == number;
    }

    public boolean isAbundant(int number) {
        return sum(factors(number)) - number > number;
    }

    public boolean isDeficiend(int number) {
        return sum(factors(number)) - number < number;
    }
}

As principais diferenças entre a Listagem 6 e a Listagem 2 estão em dois métodos: sum() e factors(). O método sum() aproveita um método na classe List no Functional Java, o método foldLeft() . Essa é uma variação específica de um conceito de manipulação de lista chamado catamorfismo, que é uma generalização do dobramento de lista. Nesse caso, "dobrar à esquerda" significa:

  1. Tomar um valor inicial e combiná-lo por meio de uma operação no primeiro elemento da lista.
  2. Tomar o resultado e aplicar a mesma operação ao elemento seguinte.
  3. Continuar fazendo isso até esgotar a lista.

Observe que isso é exatamente o que você faz quando soma uma lista de números: começa pelo zero, adiciona o primeiro elemento, toma esse resultado, adiciona-o ao segundo e continua até esgotar a lista. O Functional Java fornece a função de alta ordem (nesse exemplo, a enumeração Integers.add ) e a aplica para você. (Evidentemente, o Java não tem, de fato, funções de alta ordem, mas é possível escrever um bom analógico se você restringe a uma estrutura de dados e um tipo específico).

O outro método intrigante da Listagem 6 é factors(), que ilustra a minha recomendação de "foco nos resultados, não nas etapas". Qual é a essência do problema em descobrir os fatores de um número? Em outras palavras: dada uma lista de todos os números possíveis até um número pretendido, como eu determino quais deles são fatores do número? Isso sugere uma operação de filtragem — é possível filtrar toda a lista de números, eliminando os que não preenchem os critérios. O método basicamente lê a descrição desta forma: tome o intervalo de números de 1 até o meu número (o intervalo é não inclusivo, então o +1), filtre a lista com base no código do método f() , que é a forma que o Functional Java permite a criação de uma classe com tipos de dados específicos, e retorne os valores.

Esse código também ilustra um conceito muito maior, como uma tendência das linguagens de programação em geral. No passado, os desenvolvedores tinham que lidar com todo tipo de coisas irritantes, como alocação de memória, coleta de lixo e apontadores. Com o passar do tempo, as linguagens foram assumindo uma parte maior dessa responsabilidade. Conforme os computadores se tornaram mais eficientes, passamos uma quantidade cada vez maior de tarefas de rotina (automatizáveis) para as linguagens e tempos de execução. Como desenvolvedor Java, eu me acostumei a passar todos os problemas de memória para a linguagem. A programação funcional está expandindo essa delegação, englobando detalhes mais específicos. Com o passar do tempo, passaremos menos tempo pensando nas etapas necessárias para resolver um problema e pensaremos mais em termos de processos. No decorrer desta série, mostrarei vários exemplos disso.


Conclusão

A programação funcional é mais uma mentalidade do que um conjunto específico de ferramentas ou linguagens. Nesta primeira parte, comecei a tratar de alguns tópicos da programação funcional, de decisões simples de design a novas e ambiciosas formas de pensar nos problemas. Eu reescrevi uma classe Java simples para deixá-la mais funcional e, em seguida, passei a pesquisar alguns tópicos que distinguem a programação funcional do uso das linguagens imperativas tradicionais.

Dois conceitos importantes e de amplas implicações apareceram pela primeira vez aqui. Primeiro: foco nos resultados, não nas etapas. A programação funcional tenta apresentar problemas de forma diferente, porque você tem elementos de construção diferentes que promovem soluções. A segunda tendência que eu mostrarei ao longo desta série é a delegação dos detalhes de rotina às linguagens de programação e tempos de execução, o que permite o foco nos aspectos específicos dos nossos problemas de programação. Na próxima parte, continuarei examinando os aspectos gerais da programação funcional e sua aplicação ao desenvolvimento de software atual.

Recursos

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.
  • Mônadas: as mônadas, um bicho de sete cabeças das linguagens funcionais, serão abordadas em uma parte futura desta série.
  • Scala: o Scala é uma linguagem moderna e funcional na JVM.
  • Clojure: o Clojure é um Lisp moderno e funcional que executa na JVM.
  • Podcast: Stuart Halloway sobre Clojure: saiba mais sobre o Clojure e conheça os motivos pelos quais foi adotado e vem ganhando popularidade rapidamente.
  • Akka: o Akka é uma estrutura para Java que permite uma concorrência sofisticada baseada no ator.
  • Functional Java: o Functional Java é uma estrutura que acrescenta muitas construções de linguagem funcional ao Java.
  • Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): a obra clássica da Gang of Four sobre padrões de design.
  • "Execution in the Kingdom of Nouns" (Steve Yegge, março de 2006): um discurso irritado e divertido sobre alguns aspectos de design da linguagem Java.
  • Navegue pela livraria de tecnologia para obter livros sobre estes e outros tópicos técnicos.
  • Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

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

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

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

Elija su nombre para mostrar



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.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

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

 


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


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=661045
ArticleTitle=Pensamento Funcional: Pensando funcionalmente, Parte 1
publish-date=05262011