O estudo do conceito de mônada, à primeira vista, pode parecer uma tarefa desafiadora para aqueles que estão começando a estudar o Paradigma Funcional. Nesse artigo desmistificaremos alguns pontos em torno desse tema para torná-lo mais acessível e prático.

Pipeline e Matemática

Vejamos o programa da Listagem 1.


package exemplo01;
 
public class Exemplo01 {
    
    public static double multiplicarPor2(double n) {
        return n * 2;
    }
    
    public static double dividirPor3(double n) {
        return n / 3;    
    }
    
    public static double arredondar(double n) {
        return Math.round(n);
    }
    
    public static double aplicarOperacao(double n1) {
        final double n2 = multiplicarPor2(n1);
        final double n3 = dividirPor3(n2);
        final double n4 = arredondar(n3);
        return n4;
    }
 
    public static void main(String[] args) {                
        System.out.println("Saida = " + aplicarOperacao(12));
    }    
}
Listagem 1. Chamadas de método

Veja que a partir de um dado de entrada n1, vamos aplicando várias transformações (multiplicação, divisão e arredondamento) até obter o resultado final. Observem que dentro da função aplicarOperacao (usaremos o termo função e método sem distinções nesse artigo), a saída de uma função é a entrada da próxima, semelhante ao esquema de pipeline presente na Figura 1.

Pipeline de funções
Figura 1. Pipeline de funções

Podemos afirmar que aplicarOperacao é uma função puramente composta, pois ela foi montada exclusivamente a partir do encadeamento de outras funções. Isso é muito comum em linguagens imperativas e é um dos esquemas chave para a Programação Funcional.

O que um pipeline significa em termos matemáticos?

Vamos generalizar o exemplo da Listagem 1: dado três funções f(x), g(x) e h(x), podemos representar o pipeline da seguinte forma:


a = f(x); b = g(a); c = h(b);  ou simplesmente h(g(f(x)))

A notação h(g(f(x))) é a representação matemática de um pipeline de três funções. Vamos reescrever a função aplicarOperacao em termos dessa expressão, como mostra a Listagem 2.


package exemplo02;
 
public class Exemplo02 {
    
    public static double multiplicarPor2(double n) {
        return n * 2;
    }
    
    public static double dividirPor3(double n) {
        return n / 3;    
    }
    
    public static double arredondar(double n) {
        return Math.round(n);
    }
    
    public static double aplicarOperacao(double n1) {
        return arredondar(dividirPor3(multiplicarPor2(n1)));
    }
 
    public static void main(String[] args) {                
        System.out.println("Saida = " + aplicarOperacao(12));
    }    
}
Listagem 2. Pipeline matemático

Semanticamente falando, as funções aplicarOperacao das Listagens 1 e 2 são idênticas, sendo que o código da Listagem 2 deixa bem claro a “assinatura” matemática do pipeline e remove o uso de variáveis temporárias. Porém, usar esse tipo de notação prejudica muito a legibilidade do programa, bastando imaginar um pipeline com dez funções.

Na Programação Funcional os blocos de construção base são as funções, e a composição que vimos dentro da função aplicarOperacao é uma técnica muito comum e que ajuda a promover a reutilização de código.

Portanto, temos o seguinte dilema: como representar composições de método de uma forma elegante e de fácil leitura, sem ficar usando variáveis temporárias, como no exemplo da Listagem 1? É aqui que entra o conceito de Mônada.

Mônadas e Pipeline

Observe o código da Listagem 3.


package exemplo03;
 
import java.util.Optional;
 
public class Exemplo03 {
    
    public static Optional<Double> multiplicarPor2(double n) {
        return Optional.of(n * 2);
    }
    
    public static Optional<Double> dividirPor3(double n) {
        return Optional.of(n / 3);    
    }
    
    public static Optional<Double> arredondar(double n) {
        return Optional.of(Double.valueOf(Math.round(n)));
    }
    
    public static Optional<Double> aplicarOperacao(double n1) {
        return multiplicarPor2(n1)
                  .flatMap(n -> dividirPor3(n))
                  .flatMap(n -> arredondar(n));
    }
 
    public static void main(String[] args) {                
        System.out.println("Saida = " + aplicarOperacao(12).get());
    }    
}
Listagem 3. Usando a mônada Optional

O código da Listagem 3 faz exatamente a mesma coisa que o da Listagem 2, porém usando Optional. Percebam que, graficamente, a Figura 1 continua válida para representar o pipeline de operações da função aplicarOperacao, pois o papel de uma mônada é exatamente esse: aplicar a composição através de uma escrita encadeada plana (flat) de um nível, sendo muito mais legível do que a notação matemática aninhada de múltiplos níveis h(g(f(x))).

Então, em vez de h(g(f(x))), temos: f(x).flapMap(y => g(y)).flapMap(z => h(z)), ou seja, mônadas tornam mais legível e natural a leitura da sequência de passos (da esquerda para a direita) aplicados no dado inicial de entrada.

Esse é o maior motivo para sua presença ser tão marcante em linguagens funcionais, a ponto de ser considerado um padrão de projeto funcional e ser a base de sustentação de toda a programação declarativa do Java 8 (Optional, Stream e CompletableFuture) junto com o suporte a Lambdas e Interfaces Funcionais.

Map e FlatMap

Segundo a definição de Martin Ordersky, criador do Scala, mônada é um tipo parametrizado que deve possuir pelo menos duas operações principais:

  • unit: coloca o valor dentro da mônada (contêiner);
  • flatMap: permite chamadas encadeadas;
  • ** map: apesar de não fazer parte da definição, geralmente esse método aparece em conjunto com flatMap e é altamente recomendável disponibilizá-lo quando se cria uma mônada (veremos a seguir a razão disso).

O método unit é representado pelo método of de Optional, que coloca o parâmetro dentro da mônada (contêiner). Notem que algumas mônadas podem não ter métodos unit, mas usam outros que fazem a mesma coisa (of) ou o próprio construtor para incluir o valor diretamente no contêiner.

Já o flatMap permite chamadas encadeadas e recebe uma expressão Lambda como parâmetro, sendo o código a seguir a sua assinatura:


public<U> Optional<U> flatMap(Function<? 
super T, Optional<U>> mapper)

Qual o objetivo de uma função flatMap de uma mônada? Vejamos o seguinte exemplo da Listagem 4, onde usaremos map para explicar a flatMap.


package exemplo04;
 
import java.util.Optional;
 
public class Exemplo04 {
    
    public static void main(String[] args) {                
        Optional<Integer> optionInt = Optional.of(10);
        Optional<String> optionStr = optionInt.map(i -> i + "");
        System.out.println("saida = " + optionStr.get());
    }    
}
Listagem 4. Usando map

Veja que usamos map para aplicar uma transformação de Integer para String. Toda função map pode ser representada graficamente na mesma forma que a apresentada na Figura 2.

Representação gráfica de map
Figura 2. Representação gráfica de map

Dado uma mônada de n elementos, podemos definir map como sendo uma transformação aplicada no conjunto X, gerando os elementos correspondentes em Y, a partir do uso de uma função (lambda) transformadora F(x), mantendo a mesma quantidade de elementos.

Observem um detalhe importante: ao aplicar map sobre uma mônada, é feita a transformação do dado de entrada, e o resultado disso é encapsulado novamente na mônada. Na Listagem 4 isso fica bem claro, pois map foi aplicado sobre a mônada Optional, sendo que, como a função lambda converte um Integer para String, map retorna o resultado dentro da mesma mônada Optional, só mudando o tipo para Optional.

Map
Figura 3. Map

A Figura 3 representa graficamente o comportamento de map. É essa característica de preservar a mônada que permite aplicarmos sucessivamente operações de map (e flatMap), encadeando a saída de uma função lambda como sendo a entrada da próxima. Vamos reescrever o exemplo da Listagem 3 usando map, como mostra a Listagem 5.


package exemplo04;
 
import java.util.Optional;
 
public class Exemplo04 {
    
    public static Double multiplicarPor2(double n) {
        return n * 2;
    }
    
    public static Double dividirPor3(double n) {
        return n / 3;
    }
    
    public static Double arredondar(double n) {
        return (double) Math.round(n);
    }
    
    public static Optional<Double> aplicarOperacao(double n1) {
        return  Optional.of(n1)
                  .map(n -> multiplicarPor2(n1))
                  .map(n -> dividirPor3(n))
                  .map(n -> arredondar(n));
    }
 
    public static void main(String[] args) {                
        System.out.println("Saida = " + aplicarOperacao(12).get());
    }    
}
Listagem 5. Pipeline com map

Os programas da Listagens 3 e 6 preservam o pipeline da Figura 1,sendo que um usa flatMap e o outro map, portanto, ambos são utilizados para criar composição fluente de expressões lambdas.

Agora vejamos o seguinte exemplo da Listagem 6.


package exemplo05;
 
import java.util.Optional;
 
public class Exemplo05 {
    
    public static Optional<Double> multiplicarPor2(double n) {
        return Optional.of(n * 2);
    }
    
    public static Optional<Double> dividirPor3(double n) {
        return Optional.of(n / 3);
    }
    
    public static Optional<Double> arredondar(double n) {
        return Optional.of((double) Math.round(n));
    }    
 
    public static void main(String[] args) {                
        Optional<Optional<Double>> optMap = multiplicarPor2(12).map(n -> dividirPor3(n));
        Optional<Double> optFlatMap = multiplicarPor2(12).flatMap(n -> dividirPor3(n));        
    }    
}
Listagem 6. Map aninhado

Percebam que na linha:


Optional<Optional<Double>> optMap = 
multiplicarPor2(12).map(n -> dividirPor3(n));

A função map aplica uma transformação preservando a mônada de origem, como mostra a Figura 4.

Map na mônada de origem
Figura 4. Map na mônada de origem

Porém, como o retorno de dividirPor3(12) é Optional, teremos um aninhamento de Optional, conforme a Figura 5 mostra.

Aninhamento de Optional
Figura 5. Aninhamento de Optional

A figura apresentada deixa claro o problema de se usar map com expressões lambda que retornam mônadas. Para resolver isso devemos usar flatMap, pois ela aplica a operação de “map” e depois “flatten” (achatar, nivelar), removendo o aninhamento das duas mônadas, conforme a Figura 6.

Flatten
Figura 6. “Flatten”

Um fato importante a observar é que flatMap só aceita receber expressões lambdas que retornam a mesma mônada de origem (o tipo parametrizado pode ser diferente), enquanto map aceita lambdas que retornam qualquer coisa, inclusive outras mônadas, conforme o exemplo da Listagem 7.

 
package exemplo06;
 
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
 
public class Exemplo06 {
 
    // Compila e retorna estrutura aninhada   
    public static void testMap() {        
        Optional<Integer> optInt = Optional.of(1);
        Optional<Stream<Integer>> optStreamInt = 
        optInt.map(n -> Stream.of(n));
        Optional<CompletableFuture<Stream<
        Integer>>> optStreamComplInt =                
            optStreamInt.map(x -> 
            CompletableFuture.supplyAsync(() -> {
                return x;
            }));                        
    }    
    
    // Compila e retorna estrutura plana (flat)
    public static void testFlatMapOk() {        
        Optional<Integer> optInt1 = Optional.of(1);
        Optional<Integer> optInt2 = optInt1.flatMap(n -> 
        Optional.of(n+1));
        Optional<String> optInt3 = optInt2.flatMap(n -> 
        Optional.of(String.valueOf(n+1)));
    }            
    
    // Nao compila
    public static void testFlatMapError() {        
        Optional<Integer> optInt = Optional.of(1);
        Optional<Stream<Integer>> optStreamInt = 
        optInt.flatMap(n -> Stream.of(n));
        Optional<CompletableFuture<Stream<
        Integer>>> optStreamComplInt =                
            optStreamInt.flatMap(x -> 
            CompletableFuture.supplyAsync(() -> {
                return x;
            }));                        
    }            
}
Listagem 7. Map e flatMap

Ao trabalhar com composição de expressões lambdas, o correto é sempre usar a mesma mônada para evitar aninhamentos estranhos dessas, como vimos na Listagem 7, e possibilitar o uso de flatMap.

Mônadas e “happy path”

As mônadas podem ser usadas para implementar o conceito de “caminho feliz” (happy path), como podemos ver no exemplo com Optional da Listagem 8.


package exemplo07;
 
import java.util.Optional;
import java.util.Random;
 
public class Exemplo07 {
    
    public static Optional<Double> randomOptional(Double value) {
        return Optional.ofNullable((new Random()).nextBoolean() ? value : null);
    }    
    
    public static Optional<Double> multiplicarPor2(double n) {
        return randomOptional(n * 2);
    }
    
    public static Optional<Double> dividirPor3(double n) {
        return randomOptional(n / 3);
    }
    
    public static Optional<Double> arredondar(double n) {
        return randomOptional((double) Math.round(n));
    }
    
    public static Optional<Double> aplicarOperacao(double n1) {
        return  Optional.of(n1)
                  .flatMap(n -> multiplicarPor2(n1))
                  .flatMap(n -> dividirPor3(n))
                  .flatMap(n -> arredondar(n));
    }
 
    public static void main(String[] args) {        
        Optional opt = aplicarOperacao(12);
        if(opt.isPresent()) {        
            System.out.println("Saida = " + opt.get());
        }
    }     
}
Listagem 8. Evitando NullPointer

No artigo Programação Restritiva (ver seção Links) já abordamos o conceito de Optional e como ele pode nos ajudar a eliminar a exceção NullPointerException. Sem o uso dessa classe teríamos que fazer uma série de testes com if(obj != null) para proteger o código desse erro.

No código da Listagem 8, qualquer uma das funções pode retornar Optional.ofNullable(null), sendo que, quando isso acontece, tanto map como flatMap continuam o encadeamento, mas sem processar as expressões lambdas seguintes à expressão que gerou o resultado nulo. Para facilitar a compreensão, vamos colocar prints no código, como mostra a Listagem 9.


package exemplo08;
 
import java.util.Optional;
import java.util.Random;
 
public class Exemplo08 {
    
    public static Optional<Double> randomOptional(Double value) {
        Optional<Double> opt = 
        Optional.ofNullable((new Random()).nextBoolean() ? value : null);
        if(!opt.isPresent()) {
            System.out.print(" - Null");    
        }
        return opt;
    }    
    
    public static Optional<Double> multiplicarPor2(double n) {
        System.out.print("\nmultiplicarPor2");
        return randomOptional(n * 2);
    }
    
    public static Optional<Double> dividirPor3(double n) {
        System.out.print("\ndividirPor3");
        return randomOptional(n / 3);
    }
    
    public static Optional<Double> arredondar(double n) {
        System.out.print("\narredondar");
        return randomOptional((double) Math.round(n));
    }
    
    public static Optional<Double> aplicarOperacao(double n1) {
        return  Optional.of(n1)
                  .flatMap(n -> multiplicarPor2(n1))
                  .flatMap(n -> dividirPor3(n))
                  .flatMap(n -> arredondar(n));
    }
 
    public static void main(String[] args) {        
        Optional opt = aplicarOperacao(12);
        if(opt.isPresent()) {        
            System.out.print("\nSaida = " + opt.get());
        }
        System.out.println();
    }     
}
Listagem 9. Usando prints

Vamos analisar as saídas possíveis da Listagem 9.


// Saida 1
multiplicarPor2
dividirPor3
arredondar – Null
 
// Saida 2
multiplicarPor2 – Null
 
// Saida 3
multiplicarPor2
dividirPor3 – Null
 
// Saida 4
multiplicarPor2
dividirPor3
arredondar
Saida = 8.0

Observem que as expressões lambdas são executadas até que o primeiro null apareça, sendo que as expressões subsequentes não serão processadas (similar ao recurso do if). Mas notem que o encadeamento chega sempre a seu final, independente do null aparecer ou não (daí o nome caminho feliz). Depois basta testar o resultado final do pipeline para ver se ele falhou ou não (falhar aqui no caso significa se alguma das funções da composição teve que lidar com null).

Essa propriedade da mônada Optional permite escrever um código muito mais compacto e elegante do que a opção de ficar testando todos os retornos para ver se eles são nulos ou não. Mas não é só Optional que possui essa característica. Veremos na Listagem 10 a aplicação anterior reescrita usando Stream.


package exemplo09;
 
import java.util.Random;
import java.util.stream.Stream;
 
public class Exemplo09 {
    
    public static Stream<Double> randomStream(Double value) {        
        if(new Random().nextBoolean()) {
            return Stream.of(value);
        } else {
            System.out.print(" - Vazio");
            return Stream.empty();
        }
    }    
    
    public static Stream<Double> multiplicarPor2(double n) {
        System.out.print("\nmultiplicarPor2");
        return randomStream(n * 2);
    }
    
    public static Stream<Double> dividirPor3(double n) {
        System.out.print("\ndividirPor3");
        return randomStream(n / 3);
    }
    
    public static Stream<Double> arredondar(double n) {
        System.out.print("\narredondar");
        return randomStream((double) Math.round(n));
    }
    
    public static Stream<Double> aplicarOperacao(double n1) {
        return  Stream.of(n1)
                  .flatMap(n -> multiplicarPor2(n1))
                  .flatMap(n -> dividirPor3(n))
                  .flatMap(n -> arredondar(n));
    }
 
    public static void main(String[] args) {        
        Stream stream = aplicarOperacao(12);
        stream.forEach(n -> System.out.print("\nSaida = " + n));
        System.out.println();
    }     
}
Listagem 10. Usando Stream

No código mostrado, o conceito de “happy path” e curto-circuito continuam presente, só que em vez de null, usamos Stream.empty (ambos os conceitos representam ausência de valor). O aspecto mais interessante é que trocamos Optional por Stream sem alterar a semântica do programa, pois qualquer mônada pode ser utilizada para prover pipelines.

Nota: O conceito curto-circuito ocorre quando as expressões lambdas subsequentes não são processadas quando há uma ocorrência vazia em qualquer uma das expressões lambdas.

É claro, para comprovar essa afirmação, não poderia faltar o exemplo com CompletableFuture, como mostra a Listagem 11.


package exemplo09;
 
import java.util.Random;
import java.util.concurrent.CompletableFuture;
 
public class Exemplo09 {
    
    public static CompletableFuture<Double> 
    randomCompletableFuture(Double value) {        
        if(new Random().nextBoolean()) {            
            return CompletableFuture.completedFuture(value);
        } else {
            System.out.print(" - Vazio");
            CompletableFuture future = new CompletableFuture();
            return new CompletableFuture();
        }
    }    
    
    public static CompletableFuture<Double> 
    multiplicarPor2(double n) {
        System.out.print("\nmultiplicarPor2");
        return randomCompletableFuture(n * 2);
    }
    
    public static CompletableFuture<Double> 
    dividirPor3(double n) {
        System.out.print("\ndividirPor3");
        return randomCompletableFuture(n / 3);
    }
    
    public static CompletableFuture<Double> 
    arredondar(double n) {
        System.out.print("\narredondar");
        return randomCompletableFuture((double) 
        Math.round(n));
    }
    
    public static CompletableFuture<Double> 
    aplicarOperacao(double n1) {
        return  CompletableFuture.completedFuture(n1)
                  .thenCompose(n -> multiplicarPor2(n1))
                  .thenCompose(n -> dividirPor3(n))
                  .thenCompose(n -> arredondar(n));
    }
 
    public static void main(String[] args) {        
        CompletableFuture<Double> future = 
        aplicarOperacao(12);
        future.handle((content, ex) -> {
              if (ex == null) {
                  System.out.print("\nSaida = " + content);
              } else {
                  ex.printStackTrace();
              }
              return null;
          });
        System.out.println();        
    }     
}
Listagem 11. CompletableFuture

Esse código faz a mesma coisa que os anteriores, porém, notem que a função flatMap é chamada de thenCompose em CompletableFuture, sendo que o pipeline preserva o aspecto assíncrono das operações (seção Links).

Mônadas e contexto

Nos exemplos anteriores usamos as três mônadas para prover pipeline, porém, cada uma é usada para resolver problemas em um certo contexto:

  • Optional: trata da questão do null (NullPointerException) e ausências de valor em geral;
  • CompletableFuture: usado no contexto de computações assíncronas;
  • Stream: a base da programação declarativa com coleções, provendo um estilo mais limpo de codificação e recursos, como paralelização e lazy evaluation.

O uso do Optional confere aos programas um estilo mais defensivo e autoexplicativo ao formalizar o conceito de retorno nulo. Por exemplo, qualquer método que não tem retorno void ou primitivo pode ou não retornar null, dependendo da sua implementação. Ou seja, apenas vendo sua assinatura fica impossível definir se a função está “falando a verdade”, pois ela poderia retornar algo além do que estamos esperando (null em vez de um objeto válido indicado no tipo de retorno).

Mas e quanto às exceções não-verificadas? Notem que novamente uma função/método pode não estar sendo totalmente sincera, se nos basearmos apenas na sua assinatura, pois ela pode retornar uma exceção não-verificada e, se não tivermos acesso ao fonte, teremos que usar a instrução try/catch para desenvolver um software “seguro”.

No Scala existe a mônada Try, que tem o mesmo papel de Optional, mas aplicado para o contexto das exceções não-verificadas (não existem exceções verificadas no Scala), como mostra a Listagem 12.


import scala.util.{Failure, Success, Random, Try}
   
object TryTest extends App {
 
  def sum(a: Int, b: Int): Try[Int] = Try {
    val result = a + b
    if(Random.nextInt(2) % 2 == 0) {
      throw new RuntimeException("Error")
    }
    result
  }
 
  sum(2, 3) match {
    case Success(result) => println("Sum = " + result)
    case Failure(ex) => println(ex.getMessage)
  }
}
Listagem 12. Usando Try em Scala

Notem que, ao invés de retornar Int, a função sum retorna um Try[Int], pois a função pode retornar uma exceção, como podemos indicar através do tipo de retorno.

A mônada Try só possui duas subclasses, Failure e Success, e utilizamos pattern matching para determinar qual o tipo gerado (Success(resultado: Int) em caso de não ocorrer exceção, e Failure(ex: Exception) caso contrário).

No Github de Jason Goodwin (ver seção Links) há uma versão da mônada Try feita para Java 8. A implementação usa quatro arquivos, sendo Try a principal e as três restantes apenas interfaces de apoio, como mostra a Listagem 13 (listamos apenas Try.java no artigo, removendo o JavaDoc, para diminuir o tamanho do bloco de código).


package exemplo10;
 
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
 
public abstract class Try<T> {
 
    protected Try() {
    }
 
    public static <U> Try<U> 
    ofFailable(TrySupplier<U> f) {
        Objects.requireNonNull(f);
        try {
            return Try.successful(f.get());
        } catch (Throwable t) {
            return Try.failure(t);
        }
    }
 
    public abstract <U> Try<U> map(TryMapFunction<? 
    super T, ? extends U> f);
 
    public abstract <U> Try<U> flatMap(TryMapFunction<? 
    super T, Try<U>> f);
 
    public abstract T recover(Function<? super Throwable, T> f);
 
    public abstract Try<T> recoverWith(TryMapFunction<? 
    super Throwable, Try<T>> f);
 
    public abstract T orElse(T value);
 
    public abstract Try<T> orElseTry(TrySupplier<T> f);
 
    public abstract T get() throws Throwable;
 
    public abstract boolean isSuccess();
 
    public abstract <E extends Throwable> Try<T> 
    onSuccess(TryConsumer<T, E> action) throws E;
 
    public abstract <E extends Throwable> Try<T> 
    onFailure(TryConsumer<Throwable, E> action) throws E;
 
    public abstract Try<T> filter(Predicate<T> pred);
 
    public abstract Optional<T> toOptional();
 
    public static <U> Try<U> failure(Throwable e) {
        return new Failure<>(e);
    }
 
    public static <U> Try<U> successful(U x) {
        return new Success<>(x);
    }
}
 
class Success<T> extends Try<T> {
 
    private final T value;
 
    public Success(T value) {
        this.value = value;
    }
 
    @Override
    public <U> Try<U> 
    flatMap(TryMapFunction<? super T, Try<U>> f) {
        Objects.requireNonNull(f);
        try {
            return f.apply(value);
        } catch (Throwable t) {
            return Try.failure(t);
        }
    }
 
    @Override
    public T recover(Function<? super Throwable, T> f) {
        Objects.requireNonNull(f);
        return value;
    }
 
    @Override
    public Try<T> recoverWith(TryMapFunction<? 
    super Throwable, Try<T>> f) {
        Objects.requireNonNull(f);
        return this;
    }
 
    @Override
    public T orElse(T value) {
        return this.value;
    }
 
    @Override
    public Try<T> orElseTry(TrySupplier<T> f) {
        Objects.requireNonNull(f);
        return this;
    }
 
    @Override
    public T get() throws Throwable {
        return value;
    }
 
    @Override
    public <U> Try<U> map(TryMapFunction<? 
    super T, ? extends U> f) {
        Objects.requireNonNull(f);
        try {
            return new Success<>(f.apply(value));
        } catch (Throwable t) {
            return Try.failure(t);
        }
    }
 
    @Override
    public boolean isSuccess() {
        return true;
    }
 
    @Override
    public <E extends Throwable> Try<T> 
    onSuccess(TryConsumer<T, E> action) throws E {
        action.accept(value);
        return this;
    }
 
    @Override
    public Try<T> filter(Predicate<T> p) {
        Objects.requireNonNull(p);
        if (p.test(value)) {
            return this;
        } else {
            return Try.failure(new 
            NoSuchElementException("Predicate does not match for " 
            + value));
        }
    }
 
    @Override
    public Optional<T> toOptional() {
        return Optional.ofNullable(value);
    }
 
    @Override
    public <E extends Throwable> Try<T> 
    onFailure(TryConsumer<Throwable, E> action) {
        return this;
    }
}
 
class Failure<T> extends Try<T> {
 
    private final Throwable e;
 
    Failure(Throwable e) {
        this.e = e;
    }
 
    @Override
    public <U> Try<U> map(TryMapFunction<? 
    super T, ? extends U> f) {
        Objects.requireNonNull(f);
        return Try.failure(e);
    }
 
    @Override
    public <U> Try<U> flatMap(TryMapFunction<? 
    super T, Try<U>> f) {
        Objects.requireNonNull(f);
        return Try.<U>failure(e);
    }
 
    @Override
    public T recover(Function<? super Throwable, T> f) {
        Objects.requireNonNull(f);
        return f.apply(e);
    }
 
    @Override
    public Try<T> recoverWith(TryMapFunction<? 
    super Throwable, Try<T>> f) {
        Objects.requireNonNull(f);
        try {
            return f.apply(e);
        } catch (Throwable t) {
            return Try.failure(t);
        }
    }
 
    @Override
    public T orElse(T value) {
        return value;
    }
 
    @Override
    public Try<T> orElseTry(TrySupplier<T> f) {
        Objects.requireNonNull(f);
        return Try.ofFailable(f);
    }
 
    @Override
    public T get() throws Throwable {
        throw e;
    }
 
    @Override
    public boolean isSuccess() {
        return false;
    }
 
    @Override
    public <E extends Throwable> 
    Try<T> onSuccess(TryConsumer<T, E> action) {
        return this;
    }
 
    @Override
    public Try<T> filter(Predicate<T> pred) {
        return this;
    }
 
    @Override
    public Optional<T> toOptional() {
        return Optional.empty();
    }
 
    @Override
    public <E extends Throwable> Try<T> 
    onFailure(TryConsumer<Throwable, E> action) throws E {
        action.accept(e);
        return this;
    }
}
Listagem 13. Try no Java

Agora vamos refatorar o exemplo da Listagem 3 em termos dessa nova mônada, como mostra a Listagem 14.


package exemplo11;
 
import exemplo10.Try;
 
public class Exemplo11 {
    
    public static Try<Double> multiplicarPor2(double n) {
        return Try.successful(n * 2);
    }
    
    public static Try<Double> dividirPor3(double n) {
        return Try.successful(n / 3);    
    }
    
    public static Try<Double> arredondar(double n) {
        return Try.successful(Double.valueOf(Math.round(n)));
    }
    
    public static Try<Double> aplicarOperacao(double n1) {
        return multiplicarPor2(n1)
                .flatMap(n -> dividirPor3(n))
                .flatMap(n -> arredondar(n));
    }
 
    public static void main(String[] args) throws Throwable {                
        System.out.println("Saida = " + aplicarOperacao(12).get());
    }    
}
Listagem 14. Usando Try

Notem que, mesmo usando Try nesse código, a semântica de composição foi preservada (assim como foi com Optional, Stream e CompletableFuture). Para finalizar, temos a versão Java da Listagem 12 (em Scala), no código da Listagem 15.


package exemplo12;
 
import exemplo10.Try;
import java.util.Random;
 
public class Exemplo12 {
    
    public static Try<Integer> sum(int a, int b) {
        int result = a + b;
        if(new Random().nextInt(2) % 2 == 0) {
            return Try.failure(new RuntimeException("Error"));
        }
        return Try.successful(result);
    }
    
    public static void main(String[] args) {
    
        Try<Integer> result = sum(2, 3);
        if(result.isSuccess()) {
            result.onSuccess(r -> System.out.println("Sum = " + r));
        } else {
            result.onFailure(ex -> System.out.println(ex.getMessage()));
        }
    }    
}
Listagem 15. Versão Java

Exploramos nesse artigo algumas propriedades das mônadas e como elas ajudam a promover uma melhor visualização de composições de funções/métodos dentro do Java 8, além de prover semânticas que ajudam a tornar mais segura a programação do dia a dia.

Apesar da sua origem matemática (teoria das categorias, que é um ramo da matemática que lida com categorias/conjuntos e seus relacionamentos), o conceito de mônada é relativamente simples de entender e aplicar, sendo portanto, um padrão de projeto (funcional) que se tornará cada vez mais familiar e presente para os programadores Java.

Abraços e até a próxima.