Conteúdo


Java.next

Estilos de codificação funcional

Construções funcionais compartilhadas pelo Groovy, Scala e Clojure — e seus benefícios

Comments

Conteúdos da série:

Esse conteúdo é a parte # de # na série: Java.next

Fique ligado em conteúdos adicionais dessa série.

Esse conteúdo é parte da série:Java.next

Fique ligado em conteúdos adicionais dessa série.

Quando a coleta de lixo passou a ser utilizada de forma generalizada, ela eliminou categorias inteiras de problemas difíceis de depurar e possibilitou tempos de execução para gerenciar um processo que é complexo e propenso a erros para os desenvolvedores. O objetivo da programação funcional é fazer o mesmo com os algoritmos que você escreve, para que você possa trabalhar com um nível de abstração mais alto, ao mesmo tempo em que o tempo de execução realiza otimizações sofisticadas.

As linguagens Java.next não ocupam a mesma posição no espectro de linguagens entre o imperativo e o funcional, mas todas apresentam recursos e idiomas funcionais. As técnicas de programação funcional são bem definidas, mas às vezes as linguagens usam termos diferentes para conceitos funcionais idênticos, dificultando a identificação das semelhanças. Nesta parte do artigo, eu comparo os estilos de codificação funcional em Scala, Groovy e Clojure e discutir seus benefícios.

Processamento imperativo

Irei começar com um problema comum e sua solução imperativa. Suponha que você recebeu uma lista de nomes e que alguns deles são formados por um único caractere. Você deve retornar os nomes em uma cadeia de caractere delimitada por vírgulas que não contém nomes de um caractere, com cada nome alterado para letras maiúsculas. O código Java para implementar esse algoritmo aparece na Listagem 1.

Lista 1. Processamento imperativo
public class TheCompanyProcess {
    public String cleanNames(List<String> listOfNames) {
        StringBuilder result = new StringBuilder();
        for(int i = 0; i < listOfNames.size(); i++) {
            if (listOfNames.get(i).length() > 1) {
                result.append(capitalizeString(listOfNames.get(i))).append(",");
            }
        }
        return result.substring(0, result.length() - 1).toString();
    }

    public String capitalizeString(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
    }
}

Já que você tem que processar a lista inteira, a forma mais fácil de atacar o problema na Listagem 1 é dentro de um loop imperativo. Em cada um dos nomes, eu verifico se o comprimento é maior que 1 e, caso seja maior do que 1, anexo o nome alterado para letras maiúsculas na cadeia de caracteres result junto com uma vírgula à direita. O último nome da última cadeia de caractere não deve incluir a vírgula — portanto, eu a retiro do último valor de retorno.

Na programação imperativa, é recomendável realizar as operações em baixo nível. No método cleanNames() na Listagem 1, eu realizo três tarefas: eu filtro a lista para eliminar caracteres únicos, transformo a lista para alterar cada nome para letras e, em seguida, converto a lista em uma única cadeia de caractere. Em linguagens imperativas, eu sou obrigado a usar o mesmo mecanismo de baixo nível (iteração na lista) em todas as três tarefas. As linguagens funcionais reconhecem que filtrar, transformar e converter são operações comuns; sendo assim, elas oferecem uma forma de atacar os problemas sob uma perspectiva diferente.

Processamento funcional

As linguagens de programação funcionais categorizam problemas de forma diferente que as linguagens imperativas. As categorias lógicas de filtrar, transformar e converter aparecem como funções Essas funções implementam a transformação de baixo nível e dependem do desenvolvedor para customizar o comportamento da função escrevendo uma função que é passada como um parâmetro. Posso conceitualizar o problema da Listagem 1 em pseudocódigo da seguinte forma:

listOfEmps -> filter(x.length > 1) -> transform(x.capitalize) -> 
   convert(x, y -> x + "," + y)

Nas linguagens funcionais, é possível modelar essa solução conceitual sem se preocupar com os detalhes de implementação.

Em Scala, a

Listagem 2 implementa o exemplo de processamento da Listagem 1 no Scala. Ela é parecida com o pseudocódigo anterior, com os detalhes de implementação necessários.

Lista 2. Processamento em Scala
val employees = List("neal", "s", "stu", "j", "rich", "bob")
val result = employees
  .filter(_.length() > 1)
  .map(_.capitalize)
  .reduce(_ + "," + _)

Recebendo a lista de nomes, devo filtrá-la, eliminando os nomes cujo comprimento não é maior que 1. Em seguida, a saída dessa operação é alimentada na função map() , que executa o bloco de códigos fornecido em cada elemento da coleção, retornando a coleção transformada. Por fim, a coleção de saída de map() flui para a função reduce() , que combina cada elemento com base nas regras fornecidas no bloco de códigos. Nesse caso, combino cada par de elementos concatenando-os com uma vírgula inserida. Não me importa que os nomes dos parâmetros estejam em qualquer uma das três chamadas de função — portanto, posso usar o atalho prático de escala, que ignora nomes com _. A função reduce() começa pelos dois primeiros elementos, concatenando-os para produzir um único elemento, que se torna o primeiro elemento da próxima concatenação . Conforme reduce() "desce" na lista, ele desenvolve a cadeia de caractere delimitada por vírgula necessária.

Mostrei a implementação em Scala primeiro por causa de sua sintaxe razoavelmente conhecida e porque o Scala usa nomes consistentes na área — filtrar, mapear e reduzir — para os conceitos de filtrar, transformar e converter, respectivamente.

Em Groovy

O Groovy tem os mesmos recursos, mas usa nomes mais consistentes com as linguagens de script, como o Ruby. A versão em Groovy do exemplo de processo da Listagem 1 está na Listagem 3.

Lista 3. Processamento em Groovy
class TheCompanyProcess {
  public static String cleanUpNames(List listOfNames) {
    listOfNames
        .findAll {it.length() > 1}
        .collect {it.capitalize()}
        .join(',')
  }
}

Embora a Listagem 3 seja estruturalmente semelhante ao exemplo em Scala na Listagem 2, os nomes dos métodos são diferentes. O método de coleção do findAll Groovy aplica o bloco de códigos fornecido, retendo os elementos para os quais o bloco de códigos é verdadeiro . Assim como o Scala, o Groovy inclui um mecanismo de parâmetro implícito, por meio do parâmetro implícito predefinido it para blocos de códigos de argumento único. A versão em Groovy do método collect— demap— executa o bloco de códigos fornecido em cada elemento da coleção. O Groovy fornece uma função (join()) que concatena uma coleção de sequência em uma única sequência usando o delimitador fornecido — exatamente o que eu preciso para este exemplo.

Em Clojure

O Clojure é uma linguagem funcional que usa os nomes de funçãoreduce, map e filter , como mostra a Listagem 4.

Lista 4. Exemplo de processamento em Clojure
(defn process [list-of-emps]
  (reduce str (interpose "," 

      (map clojure.string/capitalize 
          (filter #(< 1 (count %)) list-of-emps)))))

A menos que você esteja acostumado a ler Clojure, a estrutura do código da Listagem 4 pode ser confusa. Lisps como o Clojure operam "de dentro para fora" — portanto, o lugar certo para começar é o valor de parâmetro final, list-of-emps. A função(filter ) do Clojure aceita dois parâmetros: uma função (nesse caso, uma função anônima) para usar para filtragem e a coleção a ser filtrada. É possível escrever uma definição de função para o primeiro parâmetro, como(fn [x] (< 1 (count x))), mas, no Clojure, é possível escrever funções anônimas de forma mais concisa. Como nos exemplos anteriores, o resultado da operação de filtragem é uma coleção menor. A função (map ) aceita a função de transformação como o primeiro parâmetro e a coleção — que nesse caso é o valor de retorno da operação (filter) — como o segundo. O primeiro parâmetro da função (map ) do Clojure frequentemente é uma função fornecida pelo desenvolvedor, mas qualquer função que aceite um parâmetro único funcionará; a função integrada capitalize corresponde ao requisito. Por fim, o resultado da operação (map ) torna-se o parâmetro de coleção para (reduce ). O primeiro parâmetro de (reduce ) é a função de combinação ((str ) aplicada ao retorno da função (interpose ) . (interpose ) insere seu primeiro parâmetro entre cada elemento da coleção (com exceção do último).

Até mesmo desenvolvedores experientes sofrem quando a funcionalidade fica aninhada demais, como acontece com a função (process ) na Listagem 4. Felizmente, o Clojure inclui macros que permitem "desenrolar" estruturas para deixá-las em uma ordem mais legível. A funcionalidade da Listagem 5 é idêntica à da versão da Listagem 4 .

Lista 5. Usando o macro de encadeamento por último do Clojure
(defn process2 [list-of-emps]
  (->> list-of-emps
       (filter #(< 1 (count %)))
       (map clojure.string/capitalize)
       (interpose ",")
       (reduce str)))

O macro thread-last do Clojure toma a operação comum de aplicar várias transformações em coleções e inverte a ordem típica da Lisp, restaurando uma leitura mais natural. da esquerda para a direita. Na Listagem 5, a coleção (list-of-emps) vem primeiro. Cada formulário subsequente no bloco é aplicado ao anterior. Um dos pontos fortes da Lisp é a flexibilidade sintática: sempre que o código se torna difícil de ler, é possível usar a sintaxe para obter legibilidade.

Vantagens da programação funcional

Em um famoso ensaio chamado "Beating the Averages", Paul Graham define o paradoxo do Blub: ele "inventa" uma linguagem imaginária chamada Blub e especula sobre a comparação de eficiência entre outras linguagens e o Blub:

Enquanto o hipotético programador em Blub examina os níveis inferiores do espectro de eficiência, ele sabe que está examinando níveis inferiores. Linguagens menos eficientes que o Blub são menos eficientes, evidentemente, porque não têm um recurso com o qual ele está acostumado. Entretanto, quando o hipotético programador em Blub examina os níveis superiores do espectro de eficiência, ele não percebe que está examinando níveis superiores. Ele vê linguagens que são simplesmente estranhas. Provavelmente ele considera que são tão eficientes quanto o Blub, mas com várias complicações. O Blub é bom o suficiente para ele, porque ele pensa em Blub.

Para muitos desenvolvedores em Java, o código da Listagem 2 parece estranho e esquisito — portanto, é difícil considerá-lo como vantajoso. No entanto, quando você para de especificar demais os detalhes sobre a realização das tarefas, você libera linguagens e tempos de execução cada vez mais inteligentes para obter melhorias eficientes. Por exemplo, o surgimento da JVM — ao livrar o desenvolvedor da preocupação com o gerenciamento de memória — abriu horizontes de R&D para a criação da coleta de lixo de última geração. Com a computação imperativa, você fica "atolado" nos detalhes do funcionamento do loop iterativo, dificultando otimizações como o paralelismo. O ato de pensar nas operações em nível mais geral (como filtrar, mapear e reduzir) separa o conceito da implementação, transformando uma modificação (como o paralelismo) de uma tarefa complexa e detalhada em uma simples mudança de API.

Pense em como deixar o código da Listagem 1 multiencadeado. Já que você está bastante envolvido nos detalhes do que acontece durante o loop de for , você também precisa lidar com o código de simultaneidade, que é complicado. Em seguida, considere a versão paralelizada do Scala, mostrada na Listagem 6.

Lista 6. Paralelizando o processo
val parallelResult = employees
  .par
  .filter(f => f.length() > 1)
  .map(f => f.capitalize)
  .reduce(_ + "," + _)

A única diferença entre a Listagem 2 e aListagem 6 é o acréscimo do método .par ao fluxo de comandos. O método .par retorna uma versão paralela da coleção na qual as operações subsequentes agem. Já que eu especifico as operações na coleção como conceitos de ordem mais alta, o tempo de execução subjacente fica livre para trabalhar mais.

Os desenvolvedores imperativos orientados a objetos tendem a pensar em reutilização no nível da classe, porque suas linguagens incentivam o uso de classes como blocos de construção. As linguagens de programação funcional tendem à reutilização no nível da função. As linguagens funcionais desenvolvem um mecanismo genérico sofisticado (como filter(), map() e reduce()) e permitem a customização por meio de funções fornecidas como parâmetros. Em linguagens funcionais, é comum transformar estruturas de dados em coleções padrão, como listas e mapas, porque, em seguida, podem ser manipuladas pelas eficientes funções integradas. Por exemplo, na área de Java, há dezenas de estruturas de processamento XML, e cada uma encapsula sua própria visão privada da estrutura XML e a entrega usando seus próprios métodos. Em linguagens como o Clojure, o XML é transformado em uma estrutura de dados padrão baseada em mapa, aberta a operações eficientes de transformação, redução e filtragem que já estão na linguagem.

Conclusão

Todas as linguagens modernas incluem ou estão incluindo construções de programação funcional, fazendo com que a programação funcional faça parte do seu futuro, inevitavelmente. Todas as linguagens Java.next implementam recursos funcionais eficientes — às vezes, com nomes e comportamentos diferentes. Nesta parte do artigo, eu ilustrei um novo estilo de codificação em Scala, Groovy e Clojure e mostrei alguns benefícios.

Na parte seguinte, eu entro em detalhes sobre como as linguagens diferem em suas implementações de filtrar, mapear e reduzir.


Recursos para download


Temas relacionados

  • Scala: o Scala é uma linguagem moderna e funcional na JVM.
  • Clojure: o Clojure é um Lisp moderno e funcional executado na JVM.
  • Lei de Deméter: saiba mais sobre uma diretriz de design para o desenvolvimento de software que não aprova o uso de listas longas para o acesso à propriedade.
  • Beating the Averages (Paul Graham, abril de 2003): leia sobre as experiências de Graham ao desenvolver o ViaWeb, o primeiro site online de e-commerce do tipo "construa você mesmo".
  • Functional thinking: explore a programação funcional na série de colunas de Neal Ford no developerWorks.
  • Language designer's notebook: nessa série do developerWorks, o arquiteto de linguagem Java Brian Goetz explora alguns problemas de design da linguagem que representaram desafios para a evolução da linguagem Java no Java SE 7, Java SE 8 e posteriores.

Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=980788
ArticleTitle=Java.next: Estilos de codificação funcional
publish-date=08152014