Tem-se falado muito sobre como "o Java 8 trouxe a Programação Funcional para o Java" - mas, o que isso realmente quer dizer?
Neste artigo, será apresentado o que significa ser funcional para uma linguagem, ou para um estilo de programação. Olhando a evolução de Java, em particular o seu sistema de tipos (type system), é possível ver como os novos recursos do Java 8, especialmente as expressões lambda, mudam este cenário e oferecem alguns dos principais benefícios da programação funcional.
O que é uma linguagem de programação funcional?
Em sua essência, uma linguagem de programação funcional é aquela que trata da mesma forma tanto o código como os dados. Isto significa que uma função deve ser um valor de primeira classe na linguagem e deve poder ser atribuída a variáveis, passada como parâmetro para funções, entre outras funcionalidades.
De fato, muitas linguagens funcionais vão ainda mais longe que isso e enxergam a computação e os algoritmos como estruturas mais fundamentais que os dados que operam. Algumas destas linguagens buscam desmembrar o estado do programa de suas funções (de uma forma que parece contrária ao desejo das linguagens orientadas a objetos, que normalmente buscam manter algoritmos e dados integrados).
Um exemplo seria a linguagem de programação Clojure. Apesar de executar sobre a Java Virtual Machine, que é baseada em classes, a Clojure é fundamentalmente uma linguagem funcional e não expõe diretamente as classes e objetos na linguagem de alto nível (embora exista uma boa interoperabilidade com Java).
Uma função Clojure, como a função de processamento de log apresentada a seguir, é um cidadão de primeira classe na linguagem e não precisa ser declarada em uma classe para existir.
(defn build-map-http-entries [log-file] (group-by :uri (scan-log-for-http-entries log-file)))
A programação funcional é útil quando os programas são escritos em termos de funções que sempre retornam a mesma saída para uma entrada específica (independente de qualquer outro estado presente no programa em execução) e que não causem efeitos colaterais ou alterem quaisquer estados do sistema. Funções que obedecem a esta restrição as vezes são chamadas de funções "puras" e se comportam da mesma forma que as funções matemáticas.
A grande vantagem das funções puras é que são mais fáceis de entender, pois seu funcionamento não depende de estado externo. As funções podem ser facilmente combinadas entre si - e isto pode ser observado em estilos de fluxos de trabalho dos desenvolvedores como no estilo Ler, Executar, Imprimir e Laço (Read, Execute, Print, Loop - REPL), comum a dialetos Lisp e outras linguagens com forte herança funcional.
Programação funcional em linguagens não funcionais
A característica de uma linguagem ser ou não funcional não é uma condição binária - ao invés disto, as linguagens existem em um espectro. Em um dos extremos desta espectro estão as linguagens que basicamente forçam a programação funcional, frequentemente proibindo estruturas de dados mutáveis. Clojure é um exemplo de linguagem que não permite a utilização de estruturas de dados mutáveis.
No entanto, existem outras linguagens onde é comum escrever programas no estilo funcional, apesar da linguagem não impor esta restrição. Um exemplo seria o Scala, que é uma mistura de linguagens orientadas a objetos e funcional e permite utilizar funções como valores, tais como:
val sqFn = (x: Int) => x * x
mantendo a sintaxe de classes e objetos que é muito próximas do Java.
No outro extremo, é possível escrever programas utilizando programação funcional em linguagens completamente não-funcionais, como é o caso do C, desde que a disciplina de programação adequada e as convenções sejam mantidas.
Com isto em mente, a programação funcional deve ser vista como uma função de dois fatores - um que é relevante para as linguagens de programação e outro que é relevante para programas escritos nesta linguagem:
1) Até que ponto a linguagem de programação utilizada auxilia ou reforça a programação funcional?
2) Como este programa específico faz uso das características funcionais fornecidas pela linguagem? Ele evita utilizar recursos não-funcionais, tais como estado mutável?
Java - um pouco de história
Java é uma linguagem com personalidade, otimizada para facilitar a legibilidade, a acessibilidade aos programadores iniciantes e para promover o suporte e a estabilidade a longo prazo. Estas decisões de design têm um custo, como exemplo a verbosidade e no sistema de tipos que pode as vezes parecer inflexível quando comparado a outras linguagens.
No entanto, o sistema de tipos de Java evoluiu, ainda que de forma relativamente lenta, ao longo da história da linguagem. Vamos dar uma olhada em algumas formas que a linguagem assumiu ao longo dos anos:
O sistema de tipos original do Java
O sistema de tipos original do Java já possui mais de 15 anos. É simples e claro, pois os tipos são de referência ou primitivos. Os tipos de referências (Reference Types) são classes, interfaces ou arrays (vetores).
- As classes são a essência da plataforma Java - uma classe é a unidade básica de funcionalidade que a plataforma Java vai carregar ou referenciar - e todo o código destinado para execução deve residir em uma classe;
- As interfaces não podem ser diretamente instanciadas, ao invés disso, uma classe que implementa a API definida pela interface deve ser construída;
- Os arrays podem armazenar tanto tipos primitivos como instâncias de classes, ou outros arrays;
- Todos os tipos primitivos são definidos pela plataforma e o programador não pode definir novos tipos primitivos.
Desde seus primeiros dias, o sistema de tipos do Java vem insistindo em um ponto muito importante, o de que cada tipo deve possuir um nome pelo qual possa ser referenciado. Esta idéia é conhecida como tipagem nominativa (nominative typing) e o Java é uma linguagem com forte tipagem nominativa.
Mesmo as chamadas "classes anônimas internas (anonymous inner classes)" possuem um tipo pelo qual o programador deve referenciá-las - o tipo da interface que implementam:
Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };
Outra forma de se dizer isto é a de que cada valor no Java é um tipo primitivo ou a instância de alguma classe.
Alternativas a tipos nomeados
Outras linguagens não têm esse fascínio por tipos nomeados. O Java não tem um conceito equivalente ao do Scala de um tipo que implementa um método específico (de uma assinatura específica). No Scala, isto poderia ser escrito da seguinte forma:
x : {def bar : String}
Lembre-se que no Scala indica-se o tipo da variável à direita (após o : ), então, isto é lido como: "x é de um tipo que possui um método chamado bar que retorna uma String". Podemos utilizar esta informação para definir um método no Scala da seguinte forma:
def showRefine(x : {def bar : String}) = { print(x.bar) }
e então, é possível definir um objeto Scala desta forma:
object barBell { def bar = "Bell" }
e ao chamar o método showRefine(barBell), o esperado será:
showRefine(barBell) Bell
Este é um exemplo de tipagem por refinamento. Os programadores que vêm de linguagens dinâmicas podem estar familiarizados com o conceito de duck typing ("se ele anda como um pato e grasna como um pato, então é um pato"). Tipagem por refinamento estrutural (Structural refinement typing) é um conceito similar, exceto que duck typing trata de tipos em tempo de execução, enquanto os tipos de refinamento estrutural funcionam em tempo de compilação.
Em linguagens que possuem a tipagem por refinamento estrutural de forma completa, estes tipos refinados podem ser utilizados em qualquer lugar que o programador possa esperar - como o tipo de parâmetro de um método. Em contraste, o Java não possui esta categoria de tipagem (desconsiderando alguns casos extremos e bizarros).
O sistema de tipos do Java 5
O lançamento do Java 5 trouxe três principais novos recursos ao sistema de tipos - tipos enumerados (enums), anotações (annotations) e tipos genéricos (generic types).
- Os tipos enumerados (enums) são similares a classes em alguns aspectos, mas eles têm a propriedade de restringir o número de instâncias existentes, e cada instância é distinta e especificada na descrição da classe. Planejado originalmente para utilização como constante de tipo seguro (typesafe constant), ao invés da prática então comum de usar números inteiros para constantes, a construção enum também permite padrões adicionais que são, por vezes, extremamente úteis;
- As anotações (annotations) estão relacionadas a interfaces - a palavra reservada para declarar uma é @interface - com o @ inicial indicando que este é um tipo de anotação. Como o nome sugere, elas são utilizadas para anotar elementos de código Java com informações adicionais que não afetam seu comportamento. Anteriormente, o Java utilizava o conceito de "interfaces de marcação" para fornecer uma forma limitada deste tipo de metadado, mas as anotações são consideravelmente mais flexíveis;
- Os tipos genéricos (generic types) do Java fornecem tipos parametrizados - a ideia de que um tipo pode funcionar como um "recipiente" para objetos de outro tipo, sem levar em conta as especificidades de exatamente qual é o tipo que está sendo contido. O tipo que se encaixa no recipiente é frequentemente chamado de tipo de parâmetro (parameter type).
Dos recursos introduzidos no Java 5, os enums e as anotações oferecem novas formas de tipos de referência que exigem tratamento especial pelo compilador e que são efetivamente separados das hierarquias de tipos existentes.
Os tipos genéricos adicionam uma complexidade significativa ao sistema de tipos do Java - muito em razão do fato de serem um recurso puramente de tempo de compilação. Isto exige que o desenvolvedor Java esteja atento aos sistemas de tipo em tempo de compilação e em tempo de execução, que são ligeiramente diferentes entre si.
Apesar dessas mudanças, a insistência do Java em tipos nominativos permaneceu. Os nomes de tipo agora incluem List<String> (lido como: "Lista de String") e Map<Class<?>, CachedObject> ("Map de Classe de Tipo Desconhecido para CachedObject"), mas estes ainda são tipos nomeados, e cada valor não primitivo ainda é uma instância de uma classe.
Recursos introduzidos no Java 6 e 7
O Java 6 foi essencialmente uma versão para melhoria de desempenho e de bibliotecas. As únicas mudanças para o sistema de tipos foi a expansão do papel das anotações e o lançamento da capacidade de processamento de anotação conectável (pluggable). Isto não impactou a maioria dos desenvolvedores Java e não disponibilizou um sistema de tipos conectáveis (pluggable type system) no Java 6.
O Java 7 não mudou materialmente o sistema de tipos. Os únicos novos recursos, todos de baixa relevância, são:
- Pequenas melhorias na inferência de tipos do compilador javac;
- Tratamento de assinatura polimórfica - utilizado como um detalhe de implementação para o recurso chamado method handles - que por sua vez, foi utilizado para implementar expressões lambda no Java 8;
- O Multi-catch fornece algumas caracterísitcas de "tipos de dados algébricos (algebraic data types)" - mas são puramente internos ao javac e não apresentam nenhuma consequência real para o programador usuário final.
O sistema de tipos do Java 8
Ao longo de sua história o Java foi essencialmente definido por seu sistema de tipos. Ele é fundamental para a linguagem e tem mantido estrita aderência aos tipos nominativos. De um ponto de vista prático, o sistema de tipos do Java não mudou muito entre o Java 5 e Java 7.
A primeira vista, poderíamos esperar que o Java 8 mudasse isso. Afinal de contas, uma expressão lambda parece nos afastar da tipagem nominativa:
() -> { System.out.println("Hello World!"); }
Este é um método, sem um nome, que não recebe parâmetros e retorna void. Ainda é perfeitamente tipado de forma estática, mas agora é anônimo.
Será que escapamos do Reino dos Substantivos? Isto é realmente uma nova forma de tipos para o Java?
A resposta é, talvez infelizmente, não. A JVM, na qual o Java e outras linguagens executam, é estritamente associada ao conceito de classes. O carregamento de classes (classloading) é fundamental para os modos de segurança e verificação da plataforma Java. Simplificando, seria muito, muito difícil conceber um tipo que não fosse, de alguma forma, representado através de uma classe.
Ao invés de criar uma nova categoria de tipos, as expressões lambda do Java 8 são automaticamente convertidas pelo compilador para uma instância de uma classe. A classe em questão é determinada por inferência de tipos. Por exemplo:
Runnable r = () -> { System.out.println("Hello World!"); };
A lambda expression do lado direito é um valor perfeitamente válido no Java 8 - mas seu tipo é inferido do valor à esquerda - portanto, ela é na verdade um valor do tipo Runnable. Entretanto, note que se uma expressão lamdba for utilizada de forma incorreta, ocorrerá um erro de compilação. A tipagem nominativa ainda é a forma utilizada pelo Java e até mesmo a introdução de lambdas não mudou isso.
Quão funcional é o Java 8?
Finalmente, vamos voltar para questão que fizemos no início do artigo - "Quão funcional é o Java 8?"
Antes do Java 8, se o desenvolvedor quisesse escrever no estilo funcional, seria necessário utilizar tipos aninhados (geralmente classes internas anônimas) como um substituto para literais de funções (function literals). As coleções de bibliotecas padrão não ajudariam muito o código e a maldição da mutabilidade estaria sempre presente.
As expressões lambda do Java 8 não transformam de forma mágica o Java em uma linguagem funcional. Em vez disso, seu efeito é o de criar uma linguagem ainda imperativa, com sistema de tipos ainda nominativos e que possui um maior suporte às expressões lambda como literais de funções. Simultaneamente, as melhorias para as coleções de bibliotecas permitiram que os desenvolvedores Java começassem a adotar expressões funcionais simples (como filter e map) para tratar código que seria de alta complexidade sem estes recursos.
O Java 8 demandou a introdução de alguns novos tipos para representar os blocos de construção básicos de fluxos de execução (pipelines) funcionais - interfaces como Predicate, Function e Consumer no pacote java.util.function. Estas adições tornaram o Java 8 capaz de permitir "programação ligeiramente funcional" - mas sua necessidade de representá-los como tipos (e sua localização no pacote utilitário, ao invés do core da linguagem) demonstra o estrangulamento que a tipagem nominativa impõe à linguagem Java e o quão distante a linguagem está da pureza de dialetos Lisp ou de outras linguagens funcionais.
Apesar de tudo o que foi exposto até aqui, este pequeno subconjunto do poder das linguagens funcionais pode muito bem ser tudo que a maioria dos desenvolvedores realmente precisa para suas atividades diárias de desenvolvimento. Para usuários avançados, outras linguagens (na JVM e em outros lugares) ainda existem e, sem dúvida, continuam a prosperar.