Os conceitos da Programação Funcional nos obrigam a repensar a forma como estruturamos as nossas aplicações atualmente, pois eles estabelecem novos parâmetros para atingirmos um nível elevado de “pureza” em nossos sistemas. Grau de pureza nesse contexto pode ser definido como sendo o percentual de uso de funções puras dentro da aplicação, pois quanto maior sua presença, mais aderente aos princípios do paradigma funcional o programa estará (veremos mais à frente a definição de funções puras).

Mesmo Java sendo uma linguagem Orientada a Objetos, podemos usar alguns conceitos existentes no Paradigma Funcional para escrever software mais robusto.

Transparência Referencial

O conceito de Transparência Referencial é bem simples de entender: dado uma função/método, podemos substituí-la pelo seu valor de retorno sem causar impacto na aplicação (isso é bem comum na matemática), como mostra a Listagem 1.

Listagem 1. Equação matemática


  f(x) = sqrt(9) + sqrt(9) + x
  f(x) = 2*sqrt(9) + x
  f(x) = 2*3 + x
  f(x) = 6 + x
  

Na equação apresentada, a função sqrt(9) (raiz quadrada de nove) pode ser substituída pelo seu valor de retorno (no caso 3) sem afetar o resultado final, sendo que isso só foi possível, pois sqrt é uma função pura. Uma função pode ser considerada pura quando segue os seguintes preceitos:


  • O seu resultado é determinado unicamente pelos seus valores de entrada, ou seja, nada no mundo externo, além da entrada, pode afetar sua saída;
  • Dado o mesmo conjunto de entrada(s), a função sempre irá gerar o mesmo resultado;
  • Não geram nenhum tipo de efeito colateral (alterar estado de parâmetros de entrada, variáveis globais, etc.);
  • Não produzem nenhum tipo de efeito de I/O (escrever em arquivo, ler do teclado, etc.).

Embora a transparência referencial seja um conceito simples, a aderência a tal princípio é bem mais complicada do que parece. Vejamos um exemplo na Listagem 2.

Listagem 2. Quebrando a transparência referencial


  package exemplo01;
   
  public class Exemplo01 {
      
      static class Tupla {
          public Tupla(int t1, int t2) {
              this.t1 = t1;
              this.t2 = t2;
          }        
          private int t1;         
          private int t2;
   
          public int getT1()        { return t1; }
          public void setT1(int t1) { this.t1 = t1; }
          public int getT2()        { return t2; }
          public void setT2(int t2) { this.t2 = t2; }
      }
      
      public static Tupla somar(Tupla t1, Tupla t2) {
          return new Tupla(t1.getT1() + t1.getT1(), t1.getT2() + t2.getT2());
      }
      
      public static void main(String[] args) {
          Tupla t1 = new Tupla(1, 1);
          Tupla t2 = new Tupla(2, 2);
          
          Tupla t3 = somar(t1, t2);
          t1.setT1(0);
          Tupla t4 = somar(t1, t2);
          
          System.out.println((t3.t1 == t4.t1 && t3.t2 == t4.t2));
      }    
  }

Se executarmos o código, a saída será false.

Agora observem o trecho a seguir:


          Tupla t3 = somar(t1, t2);
          t1.setT1(0);
          Tupla t4 = somar(t1, t2);
          System.out.println((t3.t1 == t4.t1 && t3.t2 == t4.t2));
  

Não podemos tratar esse trecho como fizemos com a equação matemática, pois t1 e t2 estão sujeitos a mutação. A transparência referencial não se aplica aqui, pois para o mesmo conjunto de dados de entrada (t1, t2), a função soma pode gerar retornos diferentes. E se o código não tivesse a linha que causa a mutação, ele respeitaria a transparência referencial? Observe a Listagem 3.

Listagem 3. É referencial?


          Tupla t3 = somar(t1, t2);
          Tupla t4 = somar(t1, t2);
          System.out.println((t3.t1 == t4.t1 && t3.t2 == t4.t2));
  

Sim, isso parece estar correto e respeita o princípio, mas o problema é bem mais sutil: imaginem o que acontece se compartilharmos as instâncias de t1 e t2 com Threads. As mutações em t1 e t2 podem acontecer a qualquer momento, afetando o resultado da operação de soma.

Portanto, o problema real reside na questão da mutabilidade da classe Tupla, e não na forma como a utilizamos no programa, pois mesmo que um programador escreva um código correto, como na Listagem 3, outro programador pode quebrar a transparência referencial usando threads ou simplesmente mudando o valor de t1 ou t2 entre as chamadas de método (ou pior, dentro do método somar).

O cerne da questão é a restritividade (Programação Restritiva) que uma linguagem impõe a seus usuários para que certos princípios sejam seguidos. Linguagens que seguem o paradigma funcional não são melhores que as linguagens imperativas, elas apenas aplicam restrições mais severas para que os princípios funcionais sejam respeitados, atendendo assim mais adequadamente os problemas que ela se propõe a resolver. Por exemplo, as linguagens funcionais lidam com estruturas imutáveis de uma forma muito mais performática do que as linguagens imperativas, permitindo que as mesmas sejam usadas como estruturas de dados “default” no dia a dia. Coleções são implementadas a partir de árvores, listas encadeadas e outras estruturas complexas que tornam o custo de cópias e outras manipulações mais baratas em termos de velocidade e uso de memória.

Em Java, podemos usar imutabilidade para seguir o princípio da transparência referencial, como mostra a Listagem 4.

Listagem 4. Imutabilidade


  package exemplo02;
   
  public class Exemplo02 {
      
      static final class Tupla {
          public Tupla(int t1, int t2) {
              this.t1 = t1;
              this.t2 = t2;
          }        
          private final int t1;         
          private final int t2;        
          public int getT1()        { return t1; }
          public int getT2()        { return t2; }
      }
      
      public static Tupla somar(Tupla t1, Tupla t2) {
          return new Tupla(t1.getT1() + t1.getT1(), t1.getT2() + t2.getT2());
      }
      
      public static void main(String[] args) {
          Tupla t1 = new Tupla(1, 1);
          Tupla t2 = new Tupla(2, 2);
          Tupla t3 = somar(t1, t2);
          Tupla t4 = somar(t1, t2);
          System.out.println((t3.t1 == t4.t1 && t3.t2 == t4.t2));
      }    
  }
  

A classe Tupla agora é imutável, garantindo que a operação de soma sempre retorne os mesmos resultados para as mesmas entradas, ou seja, t3 == t4 é igual (matematicamente falando) em qualquer instante de tempo.

A transparência referencial permite otimizações importantes para o compilador em linguagens funcionais. Por exemplo, se somar(t1, t2) segue esse princípio, o compilador poderia economizar uma chamada de método:


          Tupla x = somar(t1, t2);
          Tupla t3 = x;
          Tupla t4 = x;       
  

Uma vez que o resultado de somar(t1, t2) é constante em qualquer período de tempo, o valor x poderia substituir qualquer chamada à função somar(t1, t2), reduzindo o custo de processamento e invocação (o compilador Haskell faz muitas otimizações desse tipo).

O Java 8 mostra uma tendência interessante: a cada nova versão da linguagem é bem provável que o suporte aos recursos funcionais melhore ainda mais, tanto em nível de linguagem quanto em otimizações no compilador. Ou seja, mesmo que os programadores Java não escrevam programas no estilo funcional hoje, aderir a certos conceitos como imutabilidade podem facilitar a migração de sistemas que um dia tenham que possuir essas características, principalmente para lidar com a problemática questão da programação multithread e paralela.

Quando lemos livros/artigos que ressaltam a necessidade de se usar imutabilidade, minimizar exposição de estado, criar cópias defensivas e não criar getter e setter indiscriminadamente apenas para atender o padrão JavaBeans, o objetivo final é o mesmo: minimizar ao máximo as mutações, pois elas podem ser geradas de qualquer ponto do sistema (localmente, em outras classes, pacotes, módulos, dentro de threads, etc.), tornando a presença de bugs mais constantes e dificultando a aplicação de testes.

Na Engenharia temos a verdadeira composição de funcionalidade, pois blocos complexos são criados a partir de estruturas mais simples. O sucesso da composição se deve ao grau de confiabilidade das estruturas menores, que permitem propagar essa confiabilidade dentro das estruturas maiores.

A Programação Funcional tenta emular isso usando as funções como blocos de montagem básico. Se uma função tem a propriedade de transparência referencial, ela adquire grau de confiabilidade suficiente para ser a base de montagem para funções mais complexas, que por sua vez formarão outras ainda mais complexas, sendo que toda essa estrutura é sustentada por uma forte base matemática (presente também na Engenharia).

Funções puras são previsíveis, pois sempre se comportarão da mesma forma em qualquer instante de tempo, por isso são blocos seguros para composição. Porém, se o nosso sistema só tivesse funções puras ele não teria nenhuma utilidade, pois operações de I/O são consideradas impuras, logo nosso programa não interagiria com o “mundo”. Por exemplo, uma função que lê do teclado não é pura, pois a cada chamada podemos ter resultados diferentes, o que quebra a transparência referencial, por isso um pouco de “impureza” é necessário para criar aplicações práticas.

O que algumas linguagens funcionais tentam fazer é isolar o comportamento “impuro” do puro através de mônadas, no caso utilizando conceitos como “I/O Monad” para restabelecer a transparência referencial. O importante aqui é estabelecer uma fronteira clara entre esses dois aspectos.

Programação Imperativa x Declarativa

Em outro artigo sobre Programação Funcional, foi abordada a diferença entre a programação imperativa e a declarativa. O ponto principal é que a programação imperativa faz muito uso de variáveis mutáveis, por isso seu uso é evitado ao máximo em linguagens funcionais.

Linguagens híbridas como o Scala permitem o uso fácil desses dois paradigmas, o que pode ser perigoso, principalmente se o intuito for realmente seguir os princípios funcionais, o que motivou Martin Ordesky (criador da linguagem) a escrever o artigo “A Post funcional Language” (ver seção Links). Em linhas gerais, podemos dizer que algumas linguagens enfatizam ao máximo evitar side-effects e mutabilidade e usar somente funções puras (Haskell, com exceção da parte de I/O), e outras (como Scala) focam mais no uso de funções, permitindo um certo grau de imperatividade controlada (quanto menor, melhor).

Já a programação declarativa em Java é centrada nas mônadas (contêineres que encapsulam computações), sendo Stream o principal foco, portanto veremos alguns códigos imperativos refatorados para uso com Stream.

Listagem 5. Inicializando um Array de Inteiros


  package exemplo03;
   
  import java.util.ArrayList;
  import java.util.List;
  import java.util.stream.Collectors;
  import java.util.stream.IntStream;
   
  public class Exemplo03 {
    
      public static void inicializarImperativo() {        
          final List<Integer> inteiros = new ArrayList<>();
          for(int i = 0; i <= 100; i++) {
              inteiros.add(i);
          }
          for(int i = 0; i <= 100; i++) {
              System.out.print(inteiros.get(i) + ",");
          }
      }
      
      public static void inicializarDeclarativo() {        
          final List<Integer> inteiros = IntStream.range(0, 101)
                                         .boxed()
                                         .collect(Collectors.toList());
          inteiros.forEach(i -> System.out.print(i + ","));
      }
     
      public static void main(String[] args) {
          System.out.println("\nImperativo");
          inicializarImperativo();
          System.out.println("\nDeclarativo");
          inicializarDeclarativo();
      }    
  }

No código da Listagem 5 inicializamos um List de Integers nas duas formas: imperativa e declarativa. A versão imperativa usa a variável i, que muda de estado a cada iteração, enquanto a versão declarativa não faz uso de nenhuma variável que muda de estado.

A lógica por trás do uso da programação declarativa é simples: se não existir um mínimo rastro de mutabilidade e side-effects no sistema, é certo que os princípios funcionais serão respeitados, por isso, mesmo uma simples variável de controle que muda de estado é mal vista por parte dos programadores funcionais (principalmente os puristas), o que causa um certo choque cultural quando programadores que estão acostumados apenas ao uso de linguagens imperativas migram para linguagens funcionais.

Na Listagem 6 vamos cronometrar esses dois métodos, em nanosegundos, usando a abordagem ortogonal descrita no artigo Métodos Default em Java, onde usamos interfaces como Mixin (análogo ao uso de Classes Abstratas):

Listagem 6. Cronometrando


  package exemplo04;
   
  import java.util.ArrayList;
  import java.util.List;
  import java.util.function.Supplier;
  import java.util.stream.Collectors;
  import java.util.stream.IntStream;
   
  interface Cronometro {
      default <T> T cronometrar(Supplier<T> supplier) {
          long start = System.nanoTime();
          T result = supplier.get();
          long end = System.nanoTime();
          System.out.println("\nNanosegundos = " + (end - start));
          return result;
      }
  }
   
  public class Exemplo04 implements Cronometro {
   
      public void inicializarImperativo() {                
          cronometrar(() -> {
              final List<Integer> inteiros = new ArrayList<>();
              for (int i = 0; i <= 100; i++) {
                  inteiros.add(i);
              }
              for (int i = 0; i <= 100; i++) {
                  System.out.print(inteiros.get(i) + ",");
              }
              return null;
          });
      }
      
      public void inicializarDeclarativo() {        
          cronometrar(() -> {
              final List<Integer> inteiros = IntStream.range(0, 101)
                      .boxed()
                      .collect(Collectors.toList());
              inteiros.forEach(i -> System.out.print(i + ","));
              return null;
          });
      }
     
      public static void main(String[] args) {
          Exemplo04 exem = new Exemplo04();
          System.out.println("\nImperativo");
          exem.inicializarImperativo();
          System.out.println("\nDeclarativo");
          exem.inicializarDeclarativo();
      }
  }

Vejamos a medição de tempo de algumas execuções do programa da Listagem 6:


  Execução 1:
  Imperativo
  Nanosegundos = 3581237
  Declarativo
  Nanosegundos = 13415956
   
  Execução 2:
  Imperativo
  Nanosegundos = 3378728
  Declarativo
  Nanosegundos = 14745288
  

Era de se esperar que o código imperativo rodasse muito mais rápido que o declarativo, pois a Programação Declarativa é uma abstração em cima da Programação Imperativa, sendo que essa camada extra gera um custo maior uma vez que abrimos mão do controle fino da iteração e das transformações.

Na grande maioria das aplicações do dia a dia, isso não será um problema (até porque se isso fosse um problema, Java e Scala nem existiriam, tudo seria em C/C++). Níveis maiores de abstração geram um overhead em relação a abordagens mais diretas, mas os benefícios em se atuar numa camada conceitual mais alta geralmente compensam a perda de performance.

Porém, em sistemas aonde o tempo de resposta é um requisito vital, admitir um certo grau de impureza controlada e minimizada pode ser tolerado. Por isso, em linguagens híbridas como Scala, podemos até ter funções que internamente possuam algum estado mutável, mas que externamente respeitam a transparência referencial e portanto, podem ser usados para montar outras funções de forma segura (embora a abordagem sem mutabilidade seja preferida).

Nos próximos exemplos isolaremos a parte de impressão da tarefa em si, para não quebrarmos a transparência referencial (System.out envolve operações de I/O) e respeitarmos o Princípio da Responsabilidade Única.

Closures e Currying

Na Listagem 7 calcularemos números primos.

Listagem 7. Calculando números primos


  package exemplo05;
   
  import java.util.ArrayList;
  import java.util.List;
  import java.util.stream.IntStream;
   
  public class Exemplo05 {
    
      public static List<Integer> gerarNumerosPrimosImperativo(int quantidade) {        
          List<Integer> primos = new ArrayList<>(quantidade);
          for(int i = 2; primos.size() < quantidade; i++) {
            boolean ehPrimo = true;  
            for (final int primo: primos) {
              if (i % primo == 0) {
                  ehPrimo = false;
                  break;
              }
            }
            if(ehPrimo) {
              primos.add(i);            
            }
          }   
          return primos;
      }
      
      public static List<Integer> gerarNumerosPrimosDeclarativo(int quantidade) {  
          List<Integer> primos = new ArrayList<>(quantidade);
          IntStream.iterate(2, i -> i + 1).
              filter(i -> {
                  for (final int primo: primos) {
                      if (i % primo == 0) {
                          return false;
                      }
                  }
                    return true;
              }).limit(quantidade).forEach(primos::add);      
          return primos;
      }
      
      public static void imprimir(List<Integer> list) {
          list.forEach(i -> System.out.print(i + ","));
      }
     
      public static void main(String[] args) {
          System.out.println("\nImperativo");
          imprimir(gerarNumerosPrimosImperativo(20));
          System.out.println("\nDeclarativo");
          imprimir(gerarNumerosPrimosDeclarativo(20));
      }    
  }

No código apresentado temos novamente as duas versões (imperativa e declarativa), ambas implementando uma versão simplificada do algoritmo conhecido como Crivo de Eratóstenes para encontrar números primos.

Na versão imperativa, duas variáveis mudam de estado: i e ehPrimo. Já na versão declarativa não temos mudança de estado. Mas e quanto ao forEach da expressão Lambda de filter? Observe a Listagem 8.

Listagem 8. Foreach


  for (final int primo: primos) {
     if (i % primo == 0) {
       return false;
     }
  }
  return true;

A variável primo não está sendo alterada a cada iteração? A resposta é não, pois cada iteração do recurso de foreach gera uma nova variável e um novo valor (por isso o uso de final).

Porém, essa expressão lambda acessa a variável primos (List), que não faz parte dos seus dados de entrada (no caso i), violando um dos princípios das funções puras, que é depender apenas dos seus dados de entrada.

Quando expressões lambdas acessam variáveis fora de seu escopo, elas são denominadas de Closures. Esse recurso não é novidade, pois as classes anônimas já possuem esse comportamento em versões anteriores ao Java 8.

Para limitar o uso de Closures podemos usar a técnica de currying, que consiste em transformar uma função que recebe duas ou mais entradas numa cadeia de funções que só recebem uma. Vejamos o seguinte exemplo:

f(x, y, z) => x + y + z

Essa função pode ser vista da seguinte forma:

f(x)(y)(x) => (x => (y => (z => x + y + z)))

O que isso significa? Significa que, para os valores x=1, y=2 e z=3, temos:


  f(1) = g(y, z) = 1 + y + z  (aplicando currying)
  g(2) = h(z) = 1 + 2 + z (aplicando currying)
  h(3) = 1 + 2 + 3

Ou seja, a função f(x) recebe um único argumento x=1, e ao invés de retornar um valor, devolve uma nova função, no caso g(y,z) = 1 + y + z. Ao aplicar currying novamente, temos que g(y) recebe como valor y=2 e retorna uma nova função h(z) = 1 + 2 + z. Como h(z) só possui uma entrada, ela é resolvida diretamente, h(3) = 1 + 2 + 3 = 6.

Vejamos na Listagem 9 como aplicar currying no código da função declarativa de primos.

Listagem 9. Aplicando currying


      public static List<Integer> gerarNumerosPrimosDeclarativo(int quantidade) {  
          List<Integer> primos = new ArrayList<>(quantidade);
          
          Function<List<Integer>, Function<Integer, Boolean>> fx =  list -> i -> {
              for (int primo : list) {
                  if (i % primo == 0) {
                      return false;
                  }
              }
              return true;
          };        
          Function<Integer, Boolean> gx = fx.apply(primos);
          
          IntStream.iterate(2, i -> i + 1)
                   .filter(i -> gx.apply(i))
                   .limit(quantidade)
                   .forEach(primos::add);      
          return primos;        
      }

Para eliminar a Closure fizemos o seguinte passo: a função lambda que antes só recebia i, agora recebe i e primos como argumento, na forma:

f(primos, i) = Z(primos, i)

Onde Z é a função da Listagem 8.

Aplicando currying, temos:

f(''primos) => g(''primos, i) => Z(''primos, i)
  g(''i) => Z(''primos, ''i)

Para visualizar melhor currying com duas entradas, temos:


  f(x, y) => x + y
  Para x =1 e y = 2, temos:
  f(x) => x => x + y
  f(1) => (1 => 1 + y)

Ou seja, f(1) retorna como resultado uma nova função g(y) = 1 + y

g(2) = 1 + 2 = 3

Agora basta imaginar x = primos e y = i

Usamos a interface Function para declarar o tipo da variável que recebe lambda, conforme a sintaxe: Function, onde N1 representa o argumento de entrada do Lambda e NF é o valor de retorno. Portanto, quando escrevemos isso:

Function<List<Integer>, Function<Integer, Boolean>>

Estamos dizendo que o valor de entrada é um List e o retorno é outra função Lambda cuja assinatura é Function (recebe um Integer e retorna um Boolean).

Notem que no código da Listagem 9 filter recebe um lambda com um único argumento de entrada, sendo que a variável primos está embutida implicitamente dentro do contexto de gx:

filter(i -> gx.apply(i))  // == filter(i => gx(i))

A expressão lambda no método funcional forEach também é uma Closure:


  forEach(primos::add);   // Pois ela depende de i e primos   
  // Poderíamos escrever dessa forma para ficar mais claro
  forEach(i -> primos.add(i));

Então podemos aplicar a mesma lógica para eliminar essa Closure, como mostra a Listagem 10.

Listagem 10. Eliminando todas as Closures do código


      public static List<Integer> gerarNumerosPrimosDeclarativo(int quantidade) {  
          List<Integer> primos = new ArrayList<>(quantidade);
          
          Function<List<Integer>, Predicate<Integer>> fx =  list -> i -> {
              for (int primo : list) {
                  if (i % primo == 0) {
                      return false;
                  }
              }
              return true;
          };        
          Predicate<Integer> gx = fx.apply(primos);
          
          Function<List<Integer>, Consumer<Integer>> hx = list -> i -> list.add(i);
          Consumer<Integer> ix = hx.apply(primos);
          
          IntStream.iterate(2, i -> i + 1)
                   .filter(i -> gx.test(i))
                   .limit(quantidade)
                   .forEach(i -> ix.accept(i));      
          return primos;        
      }

No código, forEach recebe uma expressão lambda com um único valor de entrada, sendo que para isso aplicamos currying também. Observem que usamos as interfaces funcionais Predicate e Consumer, já predefinidas no Java, pois elas substituem:


  • Function = Predicate
  • Function = Consumer

Agora os métodos funcionais de Stream recebem lambdas que dependem unicamente dos seus valores de entrada, sendo que a dependência por primos foi encapsulada a partir do processo de currying.

Por fim, se o código do foreach ainda lhe parece muito imperativo (o bloco da Listagem 8), podemos refatorar usando Stream também:

i -> return primos.stream().noneMatch((primo) -> (i % primo == 0))

No código noneMatch verifica se nenhum item da lista atende ao critério passado, retornando true em caso afirmativo, ou false se pelo menos um dos itens atender ao critério.

Além de noneMatch, temos as funções anyMatch e allMatch da Listagem 11.

Listagem 11. Funções Match


  package exemplo08;
   
  import java.util.ArrayList;
  import java.util.List;
   
  public class Exemplo08 {
    
      public static void main(String[] args) {
          
          final List<String> nomes = new ArrayList<>();
          nomes.add("Angelica");
          nomes.add("Ana");
          nomes.add("Alberto");
          nomes.add("André");
          nomes.add("Aline");
          
          System.out.println(nomes.stream().anyMatch((s) -> s.startsWith("A")));  // true
          System.out.println(nomes.stream().anyMatch((s) -> s.startsWith("B")));  // false
          
          System.out.println(nomes.stream().noneMatch((s) -> s.startsWith("A"))); // false
          System.out.println(nomes.stream().noneMatch((s) -> s.startsWith("B"))); // true
          
          System.out.println(nomes.stream().allMatch((s) -> s.startsWith("A")));  // true
          System.out.println(nomes.stream().allMatch((s) -> s.startsWith("B")));  // false
      }    
  }

No código vemos que:


  • anyMatch retorna true se pelo menos um item atender ao critério (não continua a analisar após achar a primeira ocorrência);
  • noneMatch retorna true se nenhum item atender ao critério (não continua a analisar após achar a primeira ocorrência que quebra a premissa);
  • allMatch retorna true somente se todos os itens atenderem ao critério (não continua a analisar após achar a primeira ocorrência que quebra a premissa);

Stream possui uma série de recursos interessantes que permitem a adoção da programação declarativa no Java (ver Seção Links), utilizando recursos como lazy evaluation (avaliação tardia ou “preguiçosa”) para tornar o processamento do pipeline de operações encadeadas de Stream mais performático. Com a adoção desse estilo de programação é possível eliminar muitas variáveis de controle mutáveis e facilitar a visualização de trechos onde podemos aplicar mais efetivamente o recurso de paralelismo.

Portanto, no Java 8 podemos usar conceitos funcionais como mônadas (Stream, Optional, CompletableFuture), imutabilidade, currying, lambdas, programação declarativa e transparência referencial para tornar nossos sistemas mais aderentes ao paradigma funcional, e assim resolver diversos tipos de problemas onde esse conceito é mais vantajoso que a abordagem imperativa (paralelismo e concorrência).

O campo da Programação Funcional é bem vasto, pois como as funções são os blocos básicos de construção, as linguagens funcionais disponibilizam uma série de operadores, transformadores, estruturas, combinadores e outras ferramentas matemáticas para facilitar a composição de funções (muitos conceitos oriundos da Teoria das Categorias, um ramo da matemática que trata de objetos e morfismos). Por exemplo, no Scala temos as bibliotecas Scalaz e Shapeless, que disponibilizam uma série de estruturas funcionais, conceitos e operadores como Functors, Monads, Monoids, Applicatives, TypeClasses, Semigroups, Kleisli, Reader, State, Lenses, I/O Monads, HLists, Trees, Zipper, Iteratee, etc. para sustentar um modelo de programação funcional bem mais puro, matemático, restrito e modular (à primeira vista são tópicos bem desafiadores), o que demonstra a riqueza de conceitos que esse paradigma pode nos proporcionar.

Obrigado e até a próxima!

Links

A PostFunctional Language
http://www.scala-lang.org/old/node/4960

Haskell meets Java
http://www.seas.upenn.edu/~cis194/fall14/lectures/14-java.html

Introdução a Haskell
http://haskell.tailorfontela.com.br/introduction

Java Magazine 135: Stream API
//www.devmedia.com.br/streams-api-trabalhando-com-colecoes-de-forma-flexivel-em-java/31980