Introdução ao Vavr

Introdução ao Vavr

*1. Visão geral *

Neste artigo, exploraremos exatamente o que é Vavr, por que precisamos e como usá-lo em nossos projetos.

O Vavr é uma* biblioteca funcional para Java 8+ que fornece tipos de dados imutáveis ​​e estruturas de controle funcional. *

====* 1.1 Dependência Maven *

Para usar o Vavr, você precisa adicionar a dependência:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

É recomendável usar sempre a versão mais recente. Você pode obtê-lo seguindo este link.

===* 2. Opção *

O objetivo principal da Option é eliminar verificações nulas em nosso código, aproveitando o sistema do tipo Java.

Option é um contêiner de objetos no Vavr com um objetivo final semelhante, como link:/java-optional [Opcional] no Java 8. Option do Vavr implementa _Serializable, Iterable, _ e possui uma API mais rica .

Como qualquer referência a objeto em Java pode ter um valor null, geralmente precisamos verificar a nulidade com as instruções if antes de usá-lo. Essas verificações tornam o código robusto e estável:

@Test
public void givenValue_whenNullCheckNeeded_thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

Sem verificações, o aplicativo pode falhar devido a um simples _NPE: _

@Test(expected = NullPointerException.class)
public void givenValue_whenNullCheckNeeded_thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

No entanto, as verificações tornam o código* detalhado e não tão legível *, especialmente quando as instruções if acabam sendo aninhadas várias vezes.

Option resolve esse problema eliminando totalmente nulls e substituindo-os por uma referência de objeto válida para cada cenário possível.

Com Option, um valor null será avaliado para uma instância de None, enquanto um valor não nulo será avaliado para uma instância de Some:

@Test
public void givenValue_whenCreatesOption_thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

Portanto, em vez de usar valores de objetos diretamente, é recomendável agrupá-los em uma instância Option, como mostrado acima.

Observe que não tivemos que fazer uma verificação antes de chamar toString, mas não tivemos que lidar com uma NullPointerException como havíamos feito antes. ToString da opção nos retorna valores significativos em cada chamada.

No segundo trecho desta seção, precisamos de uma verificação null, na qual atribuímos um valor padrão à variável, antes de tentar usá-lo. Option pode lidar com isso em uma única linha, mesmo se houver um nulo:

@Test
public void givenNull_whenCreatesOption_thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);

    assertEquals("", nameOption.getOrElse(""));
}

Ou um não nulo:

@Test
public void givenNonNull_whenCreatesOption_thenCorrect() {
    String name = "";
    Option<String> nameOption = Option.of(name);

    assertEquals("", nameOption.getOrElse("not"));
}

Observe como, sem null checks, podemos obter um valor ou retornar um padrão em uma única linha.

*3. Tupla *

Não há equivalente direto de uma estrutura de dados de tupla em Java. Uma tupla é um conceito comum em linguagens de programação funcional. As tuplas são imutáveis ​​e podem conter vários objetos de tipos diferentes de maneira segura.

O Vavr traz tuplas para o Java 8. As tuplas são do tipo Tuple1, Tuple2 a Tuple8, dependendo do número de elementos que eles devem usar.

Atualmente, há um limite superior de oito elementos. Acessamos elementos de uma tupla como tuple._n, onde n é semelhante à noção de um índice em matrizes:

public void whenCreatesTuple_thenCorrect1() {
    Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
    String element1 = java8._1;
    int element2 = java8._2();

    assertEquals("Java", element1);
    assertEquals(8, element2);
}

Observe que o primeiro elemento é recuperado com n == 1. Portanto, uma tupla não usa uma base zero como uma matriz. Os tipos dos elementos que serão armazenados na tupla devem ser declarados em sua declaração de tipo, como mostrado acima e abaixo:

@Test
public void whenCreatesTuple_thenCorrect2() {
    Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
    String element1 = java8._1;
    int element2 = java8._2();
    double element3 = java8._3();

    assertEquals("Java", element1);
    assertEquals(8, element2);
    assertEquals(1.8, element3, 0.1);
}

O lugar de uma tupla é armazenar um grupo fixo de objetos de qualquer tipo que sejam melhor processados ​​como uma unidade e possam ser distribuídos. Um caso de uso* mais óbvio está retornando mais de um objeto de uma função ou método *em Java.

===* 4. Try *

No Vavr,* Try é um contêiner para um cálculo * que pode resultar em uma exceção.

Como Option agrupa um objeto nulo para que não tenhamos que cuidar explicitamente de nulos_ com if checks, Try agrupa uma computação para que não tenhamos que cuidar explicitamente de exceções com blocos try-catch.

Pegue o seguinte código, por exemplo:

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenThrowsException_thenCorrect() {
    int i = 1/0;
}

Sem os blocos try-catch, o aplicativo falharia. Para evitar isso, seria necessário agrupar a instrução em um bloco try-catch. Com o Vavr, podemos agrupar o mesmo código em uma instância Try e obter um resultado:

@Test
public void givenBadCode_whenTryHandles_thenCorrect() {
    Try<Integer> result = Try.of(() -> 1/0);

    assertTrue(result.isFailure());
}

Se a computação foi bem-sucedida ou não, pode ser inspecionada por opção em qualquer ponto do código.

No snippet acima, optamos por simplesmente verificar se há sucesso ou fracasso. Também podemos optar por retornar um valor padrão:

@Test
public void givenBadCode_whenTryHandles_thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1/0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

Ou mesmo para lançar explicitamente uma exceção de nossa escolha:

@Test(expected = ArithmeticException.class)
public void givenBadCode_whenTryHandles_thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1/0);
    result.getOrElseThrow(ArithmeticException::new);
}

Em todos os casos acima, temos controle sobre o que acontece após o cálculo, graças ao Try de Vavr.

*5. Interfaces Funcionais *

Com a chegada do Java 8, o link:/java-8-functional-interfaces [interfaces funcionais] é incorporado e mais fácil de usar, especialmente quando combinado com lambdas.

No entanto, o Java 8 fornece apenas duas funções básicas. Um leva apenas um único parâmetro e produz um resultado:

@Test
public void givenJava8Function_whenWorks_thenCorrect() {
    Function<Integer, Integer> square = (num) -> num* num;
    int result = square.apply(2);

    assertEquals(4, result);
}

O segundo usa apenas dois parâmetros e produz um resultado:

@Test
public void givenJava8BiFunction_whenWorks_thenCorrect() {
    BiFunction<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

Por outro lado, o Vavr amplia ainda mais a idéia de interfaces funcionais em Java, suportando até oito parâmetros e incrementando a API com métodos para memorização, composição e currying.

Assim como as tuplas, essas interfaces funcionais são nomeadas de acordo com o número de parâmetros utilizados: Function0, Function1, Function2 etc. Com o Vavr, teríamos escrito as duas funções acima, assim:

@Test
public void givenVavrFunction_whenWorks_thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num *num;
    int result = square.apply(2);

    assertEquals(4, result);
}

e isto:

@Test
public void givenVavrBiFunction_whenWorks_thenCorrect() {
    Function2<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

Quando não há parâmetro, mas ainda precisamos de uma saída, no Java 8, precisamos usar o tipo Consumer, no Vavr, Function0, existe para ajudar:

@Test
public void whenCreatesFunction_thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com..vavr.VavrTest", clazzName);
}

Que tal uma função de cinco parâmetros, é apenas uma questão de usar Function5:

@Test
public void whenCreatesFunction_thenCorrect5() {
    Function5<String, String, String, String, String, String> concat =
      (a, b, c, d, e) -> a + b + c + d + e;
    String finalString = concat.apply(
      "Hello ", "world", "! ", "Learn ", "Vavr");

    assertEquals("Hello world! Learn Vavr", finalString);
}

Também podemos combinar o método de fábrica estático FunctionN.of para qualquer uma das funções para criar uma função Vavr a partir de uma referência de método. Como se tivéssemos o seguinte método sum:

public int sum(int a, int b) {
    return a + b;
}

Podemos criar uma função a partir dessa forma:

@Test
public void whenCreatesFunctionFromMethodRef_thenCorrect() {
    Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
    int summed = sum.apply(5, 6);

    assertEquals(11, summed);
}

===* 6. Coleções *

A equipe do Vavr se esforçou muito ao projetar uma nova API de coleções que atenda aos requisitos de programação funcional, ou seja, persistência, imutabilidade.

*As coleções Java são mutáveis, tornando-as uma excelente fonte de falha de programa* , especialmente na presença de simultaneidade. A interface _Collection_ fornece métodos como este:
interface Collection<E> {
    void clear();
}

Este método remove todos os elementos em uma coleção (produzindo um efeito colateral) e não retorna nada. Classes como ConcurrentHashMap foram criadas para lidar com os problemas já criados.

Essa classe não apenas adiciona zero benefícios marginais, mas também degrada o desempenho da classe cujas brechas ela está tentando preencher.

*Com imutabilidade, obtemos segurança de threads de graça* : não há necessidade de escrever novas classes para lidar com um problema que não deveria estar lá em primeiro lugar.

Outras táticas existentes para adicionar imutabilidade a coleções em Java ainda criam mais problemas, a saber, exceções:

@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows_thenCorrect() {
    java.util.List<String> wordList = Arrays.asList("abracadabra");
    java.util.List<String> list = Collections.unmodifiableList(wordList);
    list.add("boom");
}

Todos os problemas acima são inexistentes nas coleções do Vavr.

Para criar uma lista no Vavr:

@Test
public void whenCreatesVavrList_thenCorrect() {
    List<Integer> intList = List.of(1, 2, 3);

    assertEquals(3, intList.length());
    assertEquals(new Integer(1), intList.get(0));
    assertEquals(new Integer(2), intList.get(1));
    assertEquals(new Integer(3), intList.get(2));
}

APIs também estão disponíveis para realizar cálculos na lista em vigor:

@Test
public void whenSumsVavrList_thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

As coleções do Vavr oferecem a maioria das classes comuns encontradas no Java Collections Framework e, na verdade, todos os recursos são implementados.

O objetivo é imutabilidade , remoção de tipos de retorno nulos e APIs produtoras de efeitos colaterais , um conjunto mais rico de funções para operar nos elementos subjacentes , código muito curto, robusto e compacto em comparação com a coleção do Java operações.

Uma cobertura completa das coleções do Vavr está além do escopo deste artigo.

*7. Validação *

O Vavr traz o conceito de Applicative Functor para Java do mundo da programação funcional. No mais simples dos termos,* um Applicative Functor nos permite executar uma sequência de ações enquanto acumula os resultados *.

A classe vavr.control.Validation facilita a acumulação de erros. Lembre-se de que, geralmente, um programa termina assim que um erro é encontrado.

No entanto, Validation continua processando e acumulando os erros para o programa atuar neles como um lote.

Considere que estamos registrando usuários por name e age e queremos receber todas as entradas primeiro e decidir se deve criar uma instância Person ou retornar uma lista de erros. Aqui está nossa classe Person:

public class Person {
    private String name;
    private int age;

   //standard constructors, setters and getters, toString
}

Em seguida, criamos uma classe chamada PersonValidator. Cada campo será validado por um método e outro método pode ser usado para combinar todos os resultados em uma instância Validation:

class PersonValidator {
    String NAME_ERR = "Invalid characters in name: ";
    String AGE_ERR = "Age must be at least 0";

    public Validation<Seq<String>, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        String invalidChars = name.replaceAll("[a-zA-Z ]", "");
        return invalidChars.isEmpty() ?
          Validation.valid(name)
            : Validation.invalid(NAME_ERR + invalidChars);
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < 0 ? Validation.invalid(AGE_ERR)
          : Validation.valid(age);
    }
}

A regra para age é que ele deve ser um número inteiro maior que 0 e a regra para name é que não deve conter caracteres especiais:

@Test
public void whenValidationWorks_thenCorrect() {
    PersonValidator personValidator = new PersonValidator();

    Validation<List<String>, Person> valid =
      personValidator.validatePerson("John Doe", 30);

    Validation<List<String>, Person> invalid =
      personValidator.validatePerson("John? Doe!4", -1);

    assertEquals(
      "Valid(Person [name=John Doe, age=30])",
        valid.toString());

    assertEquals(
      "Invalid(List(Invalid characters in name: ?!4,
        Age must be at least 0))",
          invalid.toString());
}
*Um valor válido está contido em uma instância _Validation.Valid_, uma lista de erros de validação está contida em uma instância _Validation.Invalid_* . Portanto, qualquer método de validação deve retornar um dos dois.

Dentro de Validation.Valid é uma instância de Person enquanto dentro de Validation.Invalid é uma lista de erros.

*8. Preguiçoso *

Lazy é um contêiner que representa um valor calculado preguiçosamente, isto é, o cálculo é adiado até que o resultado seja necessário. Além disso, o valor avaliado é armazenado em cache ou memorizado e retornado repetidamente sempre que necessário, sem repetir o cálculo:

@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());

    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());

    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

No exemplo acima, a função que estamos avaliando é Math.random. Observe que, na segunda linha, verificamos o valor e percebemos que a função ainda não foi executada. Isso ocorre porque ainda não demonstramos interesse no valor de retorno.

Na terceira linha do código, mostramos interesse no valor da computação chamando Lazy.get. Nesse ponto, a função é executada e Lazy.evaluated retorna true.

Também prosseguimos e confirmamos o bit de memorização de Lazy tentando get o valor novamente. Se a função que fornecemos fosse executada novamente, definitivamente receberíamos um número aleatório diferente.

No entanto, Lazy novamente retorna preguiçosamente o valor inicialmente calculado, conforme a asserção final confirma.

===* 9. Correspondência de padrões *

A correspondência de padrões é um conceito nativo em quase todas as linguagens de programação funcionais. Por enquanto, não existe tal coisa em Java.

Em vez disso, sempre que queremos realizar um cálculo ou retornar um valor com base na entrada que recebemos, usamos várias instruções if para resolver o código correto a ser executado:

@Test
public void whenIfWorksAsMatcher_thenCorrect() {
    int input = 3;
    String output;
    if (input == 0) {
        output = "zero";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    if (input == 3) {
        output = "three";
    }
    else {
        output = "unknown";
    }

    assertEquals("three", output);
}

De repente, podemos ver o código abrangendo várias linhas enquanto apenas verificamos três casos. Cada verificação ocupa três linhas de código. E se tivéssemos que verificar até cem casos, seriam cerca de 300 linhas, nada legal!

Outra alternativa é usar uma instrução switch:

@Test
public void whenSwitchWorksAsMatcher_thenCorrect() {
    int input = 2;
    String output;
    switch (input) {
    case 0:
        output = "zero";
        break;
    case 1:
        output = "one";
        break;
    case 2:
        output = "two";
        break;
    case 3:
        output = "three";
        break;
    default:
        output = "unknown";
        break;
    }

    assertEquals("two", output);
}

Não é melhor. Ainda temos uma média de 3 linhas por cheque. Muita confusão e potencial para erros. Esquecer uma cláusula break não é um problema no momento da compilação, mas pode resultar em erros difíceis de detectar posteriormente.

No Vavr, substituímos todo o bloco switch pelo método Match. Cada instrução case ou if é substituída por uma chamada do método Case.

Finalmente, padrões atômicos como _ $ () _ substituem a condição que avalia uma expressão ou valor. Também fornecemos isso como o segundo parâmetro para Case:

@Test
public void whenMatchworks_thenCorrect() {
    int input = 2;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"),
      Case($(3), "three"),
      Case($(), "?"));

    assertEquals("two", output);
}

Observe como o código é compacto, com média de apenas uma linha por cheque. A API de correspondência de padrões é muito mais poderosa que isso e pode fazer coisas mais complexas.

Por exemplo, podemos substituir as expressões atômicas por um predicado. Imagine que estamos analisando um comando do console para os sinalizadores help e version:

Match(arg).of(
    Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
    Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
    Case($(), o -> run(() -> {
        throw new IllegalArgumentException(arg);
    }))
);

Alguns usuários podem estar mais familiarizados com a versão abreviada (-v), enquanto outros, com a versão completa (–version). Um bom designer deve considerar todos esses casos.

Sem a necessidade de várias declarações if, resolvemos várias condições. Aprenderemos mais sobre predicados, várias condições e efeitos colaterais na correspondência de padrões em um artigo separado.

===* 10. Conclusão*

Neste artigo, apresentamos o Vavr, a popular biblioteca de programação funcional para Java 8. Abordamos os principais recursos que podemos adaptar rapidamente para melhorar nosso código.

O código fonte completo deste artigo está disponível no Github project.