Introdução ao futuro no Vavr

Introdução ao futuro no Vavr

1. Introdução

Core Java fornece uma API básica para cálculos assíncronos -Future.CompletableFuture é uma de suas implementações mais recentes.

Vavr fornece sua nova alternativa funcional para a APIFuture. Neste artigo, discutiremos a nova API e mostraremos como usar alguns de seus novos recursos.

Mais artigos sobre o Vavr podem ser encontradoshere.

2. Dependência do Maven

A APIFuture está incluída na dependência do Vavr Maven.

Então, vamos adicioná-lo ao nossopom.xml:


    io.vavr
    vavr
    0.9.2

Podemos encontrar a versão mais recente da dependência emMaven Central.

3. Future de Vavr

OFuture pode estar em um de dois estados:

  • Pendente - o cálculo está em andamento

  • Concluído - o cálculo foi concluído com êxito com um resultado, falhou com uma exceção ou foi cancelado

A principal vantagem sobre o JavaFuture principal é que podemos facilmente registrar retornos de chamada e compor operações de uma maneira sem bloqueio.

4. Operações básicasFuture

4.1. Iniciando Computações Assíncronas

Agora, vamos ver como podemos iniciar cálculos assíncronos usando Vavr:

String initialValue = "Welcome to ";
Future resultFuture = Future.of(() -> someComputation());

4.2. Recuperando Valores de aFuture

Podemos extrair valores deFuture simplesmente chamando um dos métodosget() ougetOrElse():

String result = resultFuture.getOrElse("Failed to get underlying value.");

A diferença entreget()egetOrElse() é queget() é a solução mais simples, enquantogetOrElse() nos permite retornar um valor de qualquer tipo, caso não possamos recuperar o valor dentro deFuture.

É recomendado usargetOrElse() para que possamos lidar com quaisquer erros que ocorram ao tentar recuperar o valor de umFuture. For the sake of simplicity, we’ll just use get() in the next few examples.

Observe que o métodoget() bloqueia o thread atual se for necessário esperar pelo resultado.

Uma abordagem diferente é chamar o métodogetValue() não bloqueante, que retorna umOption<Try<T>> quewill be empty as long as computation is pending.

Podemos então extrair o resultado do cálculo que está dentro do objetoTry:

Option> futureOption = resultFuture.getValue();
Try futureTry = futureOption.get();
String result = futureTry.get();

Às vezes, precisamos verificar seFuture contém um valor antes de recuperar valores dele.

Podemos simplesmente fazer isso usando:

resultFuture.isEmpty();

É importante observar que o métodoisEmpty() está bloqueando - ele bloqueará o thread até que sua operação seja concluída.

4.3. Alterando o padrãoExecutorService

Futures usa umExecutorService para executar seus cálculos de forma assíncrona. OExecutorService padrão éExecutors.newCachedThreadPool().

Podemos usar outroExecutorService passando uma implementação de nossa escolha:

@Test
public void whenChangeExecutorService_thenCorrect() {
    String result = Future.of(newSingleThreadExecutor(), () -> HELLO)
      .getOrElse(error);

    assertThat(result)
      .isEqualTo(HELLO);
}

5. Execução de ações após a conclusão

A API fornece o métodoonSuccess() que executa uma ação assim queFuture é concluído com êxito.

Da mesma forma, o métodoonFailure() é executado na falha deFuture.

Vamos ver um exemplo rápido:

Future resultFuture = Future.of(() -> appendData(initialValue))
  .onSuccess(v -> System.out.println("Successfully Completed - Result: " + v))
  .onFailure(v -> System.out.println("Failed - Result: " + v));

O métodoonComplete() aceita que uma ação seja executada assim queFuture tiver concluído sua execução, independentemente deFuture ter sido bem-sucedido ou não. O métodoandThen() é semelhante aonComplete() - ele apenas garante que os callbacks sejam executados em uma ordem específica:

Future resultFuture = Future.of(() -> appendData(initialValue))
  .andThen(finalResult -> System.out.println("Completed - 1: " + finalResult))
  .andThen(finalResult -> System.out.println("Completed - 2: " + finalResult));

6. Operações úteis emFutures

6.1. Bloqueando o Tópico Atual

O métodoawait() tem dois casos:

  • seFuture estiver pendente, ele bloqueia o thread atual até que o futuro seja concluído

  • se oFuture for concluído, ele termina imediatamente

O uso desse método é direto:

resultFuture.await();

6.2. Cancelando um cálculo

Sempre podemos cancelar o cálculo:

resultFuture.cancel();

6.3. Recuperando oExecutorService subjacente

Para obter oExecutorService que é usado porFuture, podemos simplesmente chamarexecutorService():

resultFuture.executorService();

6.4. Obtendo umThrowable de umFuture com falha

Podemos fazer isso usando o métodogetCause() que retornaThrowable embrulhado em um objetoio.vavr.control.Option.

Podemos posteriormente extrairThrowable do objetoOption:

@Test
public void whenDivideByZero_thenGetThrowable2() {
    Future resultFuture = Future.of(() -> 10 / 0)
      .await();

    assertThat(resultFuture.getCause().get().getMessage())
      .isEqualTo("/ by zero");
}

Além disso, podemos converter nossa instância emFuture contendo uma instânciaThrowable usando o métodofailed():

@Test
public void whenDivideByZero_thenGetThrowable1() {
    Future resultFuture = Future.of(() -> 10 / 0);

    assertThatThrownBy(resultFuture::get)
      .isInstanceOf(ArithmeticException.class);
}

6.5. isCompleted(), isSuccess(), eisFailure()

Esses métodos são praticamente auto-explicativos. Eles verificam se umFuture foi concluído, se foi concluído com sucesso ou com falha. Todos eles retornam valoresboolean, é claro.

Usaremos esses métodos com o exemplo anterior:

@Test
public void whenDivideByZero_thenCorrect() {
    Future resultFuture = Future.of(() -> 10 / 0)
      .await();

    assertThat(resultFuture.isCompleted()).isTrue();
    assertThat(resultFuture.isSuccess()).isFalse();
    assertThat(resultFuture.isFailure()).isTrue();
}

6.6. Aplicando Computações em um Futuro

O métodomap() nos permite aplicar um cálculo em cima de umFuture: pendente

@Test
public void whenCallMap_thenCorrect() {
    Future futureResult = Future.of(() -> "from example")
      .map(a -> "Hello " + a)
      .await();

    assertThat(futureResult.get())
      .isEqualTo("Hello from example");
}

Se passarmos uma função que retornaFuture para o métodomap(), podemos terminar com uma estruturaFuture aninhada. Para evitar isso, podemos aproveitar o métodoflatMap():

@Test
public void whenCallFlatMap_thenCorrect() {
    Future futureMap = Future.of(() -> 1)
      .flatMap((i) -> Future.of(() -> "Hello: " + i));

    assertThat(futureMap.get()).isEqualTo("Hello: 1");
}



6.7. TransformandoFutures

O métodotransformValue() pode ser usado para aplicar um cálculo sobreFuturee alterar o valor dentro dele para outro valor do mesmo tipo ou um tipo diferente:

@Test
public void whenTransform_thenCorrect() {
    Future future = Future.of(() -> 5)
      .transformValue(result -> Try.of(() -> HELLO + result.get()));

    assertThat(future.get()).isEqualTo(HELLO + 5);
}



6.8. CompactandoFutures

A API fornece o métodozip() que compactaFutures em tuplas - uma tupla é uma coleção de vários elementos que podem ou não estar relacionados entre si. Eles também podem ser de tipos diferentes. Vamos ver um exemplo rápido:

@Test
public void whenCallZip_thenCorrect() {
    Future f1 = Future.of(() -> "hello1");
    Future f2 = Future.of(() -> "hello2");

    assertThat(f1.zip(f2).get())
      .isEqualTo(Tuple.of("hello1", "hello2"));
}

O ponto a ser observado aqui é que oFuture resultante estará pendente enquanto pelo menos um dosFutures base ainda estiver pendente.

6.9. Conversão entreFutures eCompletableFutures

A API oferece suporte à integração comjava.util.CompletableFuture. Portanto, podemos facilmente converterFuture emCompletableFuture se quisermos realizar operações que apenas a API Java principal suporta.

Vamos ver como podemos fazer isso:

@Test
public void whenConvertToCompletableFuture_thenCorrect()
  throws Exception {

    CompletableFuture convertedFuture = Future.of(() -> HELLO)
      .toCompletableFuture();

    assertThat(convertedFuture.get())
      .isEqualTo(HELLO);
}

Também podemos converter aCompletableFuture emFuture usando o métodofromCompletableFuture().

6.10. Manipulação de exceção

Em caso de falha de aFuture, podemos lidar com o erro de algumas maneiras.

Por exemplo, podemos usar o métodorecover() para retornar outro resultado, como uma mensagem de erro:

@Test
public void whenFutureFails_thenGetErrorMessage() {
    Future future = Future.of(() -> "Hello".substring(-1))
      .recover(x -> "fallback value");

    assertThat(future.get())
      .isEqualTo("fallback value");
}

Ou podemos retornar o resultado de outro cálculo deFuture usandorecoverWith():

@Test
public void whenFutureFails_thenGetAnotherFuture() {
    Future future = Future.of(() -> "Hello".substring(-1))
      .recoverWith(x -> Future.of(() -> "fallback value"));

    assertThat(future.get())
      .isEqualTo("fallback value");
}

O métodofallbackTo() é outra maneira de lidar com erros. Ele é chamado emFuturee aceita outroFuture como parâmetro.

Se o primeiroFuture for bem-sucedido, ele retornará seu resultado. Caso contrário, se o segundoFuture for bem-sucedido, ele retornará seu resultado. Se ambosFutures falharem, então o métodofailed() retorna umFuture de umThrowable, que contém o erro do primeiroFuture:

@Test
public void whenBothFuturesFail_thenGetErrorMessage() {
    Future f1 = Future.of(() -> "Hello".substring(-1));
    Future f2 = Future.of(() -> "Hello".substring(-2));

    Future errorMessageFuture = f1.fallbackTo(f2);
    Future errorMessage = errorMessageFuture.failed();

    assertThat(
      errorMessage.get().getMessage())
      .isEqualTo("String index out of range: -1");
}

7. Conclusão

Neste artigo, vimos o que é aFuture e aprendemos alguns de seus conceitos importantes. Também examinamos alguns dos recursos da API usando alguns exemplos práticos.

A versão completa do código está disponívelover on GitHub.