Programação Java funcional com Vavr

Muitos já ouviram falar de linguagens funcionais como Haskell e Clojure. Mas existem idiomas como o Scala, por exemplo. Combina OOP e uma abordagem funcional. E o bom e velho Java? É possível escrever programas em um estilo funcional e o quanto isso pode prejudicar? Sim, há Java 8 e lambdas com fluxos. Este é um grande passo para o idioma, mas ainda não é suficiente. É possível inventar algo nessa situação? Acontece que sim.



Para começar, vamos tentar determinar o que significa a escrita de código em um estilo funcional. Primeiro, devemos operar não com variáveis ​​e manipulações com elas, mas com cadeias de alguns cálculos. Em essência, uma sequência de funções. Além disso, precisamos ter estruturas de dados especiais. Por exemplo, coleções java padrão não são adequadas. Logo ficará claro o porquê.

Vamos considerar estruturas funcionais com mais detalhes. Qualquer estrutura desse tipo deve atender a pelo menos duas condições:

  • imutável - a estrutura deve ser imutável. Isso significa que fixamos o estado do objeto no estágio de criação e o deixamos como tal até o final de sua existência. Um exemplo claro de uma violação de condição: ArrayList padrão.
  • persistente - a estrutura deve ser armazenada na memória o maior tempo possível. Se criamos algum objeto, em vez de criar um novo com o mesmo estado, devemos usar o pronto. Mais formalmente, essas estruturas retêm todos os seus estados anteriores mediante modificação. As referências a essas condições devem permanecer totalmente operacionais.

Obviamente, precisamos de algum tipo de solução de terceiros. E existe uma solução: biblioteca Vavr . Hoje é a biblioteca Java mais popular para trabalhar em um estilo funcional. A seguir, descreverei os principais recursos da biblioteca. Muitos, mas de modo algum todos, exemplos e descrições foram extraídos da documentação oficial.

As principais estruturas de dados da biblioteca vavr


Tuple


Uma das estruturas de dados funcionais mais básicas e simples são as tuplas. Uma tupla é um conjunto ordenado de comprimento fixo. Diferentemente das listas, uma tupla pode conter dados de qualquer tipo.

Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) 

A obtenção do item desejado vem da chamada do campo com o número do item na tupla.

 ((Tuple4) tuple)._1 // 1 

Observe: a indexação da tupla começa em 1! Além disso, para obter o elemento desejado, devemos converter nosso objeto no tipo desejado com o conjunto de métodos apropriado. No exemplo acima, usamos uma tupla de 4 elementos, o que significa que a conversão deve ser do tipo Tuple4 . De fato, ninguém está nos impedindo de fazer o tipo certo inicialmente.

 Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) System.out.println(tuple._1); // 1 

As 3 principais coleções do vavr


Lista


Criar uma lista com o vavr é muito simples. Ainda mais fácil do que sem o vavr .

 List.of(1, 2, 3) 

O que podemos fazer com essa lista? Bem, em primeiro lugar, podemos transformá-lo em uma lista java padrão.

 final boolean containThree = List.of(1, 2, 3) .asJava() .stream() .anyMatch(x -> x == 3); 

Mas, de fato, isso não é muito necessário, porque podemos fazer, por exemplo, o seguinte:

 final boolean containThree = List.of(1, 2, 3) .find(x -> x == 1) .isDefined(); 

Em geral, a lista de bibliotecas vavr padrão possui muitos métodos úteis. Por exemplo, existe uma função de convolução bastante poderosa que permite combinar uma lista de valores por alguma regra e um elemento neutro.

 //   final int zero = 0; //   final BiFunction<Integer, Integer, Integer> combine = (x, y) -> x + y; //   final int sum = List.of(1, 2, 3) .fold(zero, combine); //   

Um ponto importante deve ser observado aqui. Temos estruturas de dados funcionais, o que significa que não podemos mudar seu estado. Como nossa lista é implementada? Matrizes simplesmente não combinam conosco.

Lista vinculada como a lista padrão

Vamos fazer uma lista simplesmente vinculada com objetos imutáveis. Será algo parecido com isto:

imagem

Exemplo de código
 List list = List.of(1, 2, 3); 


Cada elemento da lista possui dois métodos principais: obter o elemento principal (cabeça) e todos os outros (cauda).

Exemplo de código
 list.head(); // 1 list.tail(); // List(2, 3) 


Agora, se quisermos alterar o primeiro elemento da lista (de 1 para 0), precisamos criar uma nova lista com a reutilização das peças acabadas.

imagem
Exemplo de código
 final List tailList = list.tail(); //    tailList.prepend(0); //      


E isso é tudo! Como nossos objetos na planilha são imutáveis, obtemos uma coleção reutilizável e segura para threads. Os elementos da nossa lista podem ser aplicados em qualquer lugar do aplicativo e é totalmente seguro!

Fila


Outra estrutura de dados extremamente útil é a fila. Como criar uma fila para criar programas eficazes e confiáveis ​​em um estilo funcional? Por exemplo, podemos usar estruturas de dados já conhecidas por nós: duas listas e uma tupla.

imagem

Exemplo de código
 Queue<Integer> queue = Queue.of(1, 2, 3) .enqueue(4) .enqueue(5); 


Quando o primeiro termina, expandimos o segundo e o usamos para leitura.

imagem

imagem

É importante lembrar que a fila deve permanecer inalterada, como todas as outras estruturas. Mas qual é o uso de uma fila que não muda? De fato, há um truque. Como o valor aceito da fila, obtemos uma tupla de dois elementos. Primeiro: o elemento da fila desejado, segundo: o que aconteceu com a fila sem esse elemento.

 System.out.println(queue); // Queue(1, 2, 3, 4, 5) Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue(); System.out.println(tuple2._1); // 1 System.out.println(tuple2._2); // Queue(2, 3, 4, 5) 

Streams


A próxima estrutura de dados importante é o fluxo. Um fluxo é um fluxo de execução de algumas ações em um determinado conjunto de valores, geralmente abstrato.

Alguém pode dizer que o Java 8 já possui fluxos completos e não precisamos de novos. É isso mesmo?

Para começar, verifique se o java stream não é uma estrutura de dados funcional. Verifique a estrutura quanto a mutabilidade. Para fazer isso, crie um fluxo tão pequeno:
 IntStream standardStream = IntStream.range(1, 10); 

Classificaremos todos os elementos no fluxo:

 standardStream.forEach(System.out::print); 

Em resposta, obtemos a saída para o console: 123456789 . Vamos repetir a operação de força bruta:

 standardStream.forEach(System.out::print); 

Ops, ocorreu o seguinte erro:

 java.lang.IllegalStateException: stream has already been operated upon or closed 

O fato é que os fluxos padrão são apenas algum tipo de abstração sobre um iterador. Embora os fluxos externos pareçam extremamente independentes e poderosos, os desvios dos iteradores não desapareceram.

Por exemplo, a definição de um fluxo não diz nada sobre como limitar o número de elementos. Infelizmente, ele existe no iterador, o que significa que está nos fluxos padrão.

Felizmente, a biblioteca vavr resolve esses problemas. Certifique-se disso:

 Stream stream = Stream.range(1, 10); stream.forEach(System.out::print); stream.forEach(System.out::print); 

Em resposta, obtemos 123456789123456789 . O que significa que a primeira operação não "estragou" nosso fluxo.

Vamos tentar criar um fluxo interminável:

Stream infiniteStream = Stream.from (1);
System.out.println (infiniteStream); // Stream (1 ,?)

Observe: ao imprimir um objeto, obtemos não uma estrutura infinita, mas o primeiro elemento e um ponto de interrogação. O fato é que cada elemento subsequente no fluxo é gerado em tempo real. Essa abordagem é chamada inicialização lenta. É ele quem permite que você trabalhe com segurança com essas estruturas.

Se você nunca trabalhou com estruturas de dados infinitas, provavelmente está pensando: por que isso é necessário? Mas eles podem ser extremamente convenientes. Escrevemos um fluxo que retorna um número arbitrário de números ímpares, os converte em uma string e adiciona um espaço:

 Stream oddNumbers = Stream .from(1, 2) //  1   2 .map(x -> x + " "); //  //   oddNumbers.take(5) .forEach(System.out::print); // 1 3 5 7 9 oddNumbers.take(10) .forEach(System.out::print); // 1 3 5 7 9 11 13 15 17 19 

Tão simples.

Estrutura geral das coleções


Depois de discutirmos as estruturas básicas, é hora de examinar a arquitetura geral das coleções funcionais do vavr :



Cada elemento da estrutura pode ser usado como iterável:

 StringBuilder builder = new StringBuilder(); for (String word : List.of("one", "two", "tree")) { if (builder.length() > 0) { builder.append(", "); } builder.append(word); } System.out.println(builder.toString()); // one, two, tree 

Mas você deve pensar duas vezes e ver o encaixe antes de usar o. A biblioteca permite que você facilite coisas familiares.

 System.out.println(List.of("one", "two", "tree").mkString(", ")); // one, two, tree 

Trabalhar com funções


A biblioteca possui várias funções (8 peças) e métodos úteis para trabalhar com elas. São interfaces funcionais comuns com muitos métodos interessantes. O nome das funções depende do número de argumentos aceitos (de 0 a 8). Por exemplo, a Função0 não aceita argumentos, a Função1 aceita um argumento, a Função2 aceita dois, etc.

 Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin 

Nas funções da biblioteca vavr, podemos fazer muitas coisas legais. Em termos de funcionalidade, eles vão muito além do padrão Function, BiFunction, etc. Por exemplo, currying. Currying é a construção de funções em partes. Vejamos um exemplo:

 //    Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; //           Function1<String, String> makeGriffinName = combineName .curried() .apply("Griffin"); //      System.out.println(makeGriffinName.apply("Peter")); // Peter Griffin System.out.println(makeGriffinName.apply("Lois")); // Lois Griffin 

Como você pode ver, sucintamente. O método ao curry é extremamente simples, mas pode ser muito útil.

Implementação de método ao curry
 @Override default Function1<T1, Function1<T2, R>> curried() { return t1 -> t2 -> apply(t1, t2); } 


Existem muitos métodos mais úteis no conjunto de funções . Por exemplo, você pode armazenar em cache o resultado de retorno de uma função:

 Function0<Double> hashCache = Function0.of(Math::random).memoized(); double randomValue1 = hashCache.apply(); double randomValue2 = hashCache.apply(); System.out.println(randomValue1 == randomValue2); // true 


Luta contra exceções


Como dissemos anteriormente, o processo de programação deve ser seguro. Para fazer isso, é necessário evitar vários efeitos estranhos. Exceções são seus geradores explícitos.

Você pode usar a classe Try para manipular com segurança exceções em um estilo funcional. De fato, esta é uma mônada típica. Não é necessário se aprofundar na teoria para uso. Basta ver um exemplo simples:

 Try.of(() -> 4 / 0) .onFailure(System.out::println) .onSuccess(System.out::println); 

Como você pode ver no exemplo, tudo é bem simples. Simplesmente penduramos o evento em um erro em potencial e não o levamos além dos limites da computação.

Correspondência de padrões


Muitas vezes, surge uma situação em que precisamos verificar o valor de uma variável e modelar o comportamento do programa, dependendo do resultado. Apenas nessas situações, um maravilhoso mecanismo de busca de modelos vem em socorro. Você não precisa mais escrever muitos outros , apenas configure toda a lógica em um só lugar.

 import static io.vavr.API.*; import static io.vavr.Predicates.*; public class PatternMatchingDemo { public static void main(String[] args) { String s = Match(1993).of( Case($(42), () -> "one"), Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"), Case($(), "?") ); System.out.println(s); // two } } 

Observe que maiúsculas e minúsculas são maiúsculas, pois case é uma palavra-chave e já foi usada.

Conclusão


Na minha opinião, a biblioteca é muito legal, mas vale a pena usá-la com muito cuidado. Ela pode se sair bem no desenvolvimento orientado a eventos . No entanto, seu uso excessivo e imprudente na programação imperativa padrão baseada em um pool de threads pode trazer muita dor de cabeça. Além disso, geralmente em nossos projetos usamos o Spring e o Hibernate, que nem sempre estão prontos para esse aplicativo. Antes de importar uma biblioteca para o seu projeto, você precisa entender claramente como e por que ela será usada. Sobre o que falarei em um dos meus próximos artigos.

Source: https://habr.com/ru/post/pt421839/


All Articles