Escrevendo o Monad `Result` do Zero em Java

Escrevendo o Monad Result do Zero em Java
Nesse artigo, a gente vai passar pelo processo de criar um Monad Result em Java. A classe Result é uma classe utilitária feita pra encapsular o resultado de uma operação que pode dar certo ou errado. Ela guarda ou um valor do tipo V ou uma exceção indicando um erro. Essa classe é útil pra lidar com operações que podem dar certo ou falhar, oferecendo uma maneira clara e funcional de gerenciar os estados de sucesso e erro.
1. Definindo a Classe Result
Primeiro, vamos definir a classe Result com dois campos: value e error. Esses campos vão guardar o resultado bem-sucedido e a exceção, respectivamente.
package com.hogemann.monad;
public class Result<V> {
private final V value;
private final Exception error;
private Result(V value, Exception error) {
this.value = value;
this.error = error;
}
}
2. Adicionando Métodos Estáticos
Agora, vamos adicionar métodos estáticos pra criar instâncias da classe Result. Esses métodos funcionam como funções que encapsulam um valor ou um erro no contexto do Result.
ok(V value): Cria uma instância deResultrepresentando uma operação bem-sucedida.error(Exception error): Cria uma instância deResultrepresentando uma operação com falha.empty(): Cria uma instância vazia deResult, sem valor ou erro.
public static <V> Result<V> ok(V value) {
return new Result<>(value, null);
}
public static <V> Result<V> error(Exception error) {
return new Result<>(null, error);
}
public static <V> Result<V> empty() {
return new Result<>(null, null);
}
3. Adicionando Métodos de Instância
Vamos adicionar métodos de instância pra pegar o valor ou o erro e pra checar o estado do Result.
get(): Retorna o valor se a operação deu certo.error(): Retorna a exceção se a operação falhou.isOk(): Verifica se a operação foi bem-sucedida.isEmpty(): Verifica se o resultado tá vazio.
public V get() {
return this.value;
}
public Exception error() {
return this.error;
}
public boolean isOk() {
return this.error == null && this.value != null;
}
public boolean isEmpty() {
return this.value == null && this.error == null;
}
4. Adicionando Operações de Mônada
Pra fazer da classe Result um Monad, precisamos adicionar os métodos map e flatMap. Esses métodos permitem encadear operações enquanto mantém o contexto.
map(Function<V,U> mapper): Transforma o valor usando a função passada se a operação foi bem-sucedida.flatMap(Function<V,Result<U>> mapper): Transforma o valor em outroResultusando a função passada se a operação deu certo.
public <U> Result<U> map(Function<V, U> mapper) {
if (isOk()) {
return ok(mapper.apply(value));
} else {
return error(error);
}
}
public <U> Result<U> flatMap(Function<V, Result<U>> mapper) {
if (isOk()) {
return mapper.apply(value);
} else {
return error(error);
}
}
5. Adicionando Métodos Utilitários
Por fim, a gente vai adicionar um método utilitário pra executar ações de acordo com o resultado da operação.
ifOkOrElse(Consumer<V> consumer, Consumer<Exception> errorConsumer): Executa o consumer certo dependendo se a operação foi bem-sucedida ou não.
public void ifOkOrElse(Consumer<V> consumer, Consumer<Exception> errorConsumer) {
if (isOk()) {
consumer.accept(value);
} else {
errorConsumer.accept(error);
}
}
Classe Result Completa
Aqui tá a classe Result completa:
package com.hogemann.monad;
import java.util.function.Consumer;
import java.util.function.Function;
public class Result<V> {
private final V value;
private final Exception error;
private Result(V value, Exception error) {
this.value = value;
this.error = error;
}
public static <V> Result<V> ok(V value) {
return new Result<>(value, null);
}
public static <V> Result<V> error(Exception error) {
return new Result<>(null, error);
}
public static <V> Result<V> empty() {
return new Result<>(null, null);
}
public V get() {
return this.value;
}
public Exception error() {
return this.error;
}
public boolean isOk() {
return this.error == null && this.value != null;
}
public boolean isEmpty() {
return this.value == null && this.error == null;
}
public <U> Result<U> map(Function<V, U> mapper) {
if (isOk()) {
return ok(mapper.apply(value));
} else {
return error(error);
}
}
public <U> Result<U> flatMap(Function<V, Result<U>> mapper) {
if (isOk()) {
return mapper.apply(value);
} else {
return error(error);
}
}
public void ifOkOrElse(Consumer<V> consumer, Consumer<Exception> errorConsumer) {
if (isOk()) {
consumer.accept(value);
} else {
errorConsumer.accept(error);
}
}
}
Exemplos de Uso
Pra mostrar como usar o Monad Result, vamos ver alguns exemplos práticos.
Exemplo 1: Lidando com uma Operação Bem-Sucedida
Nesse exemplo, vamos simular uma operação que deu certo e retorna um Result contendo um valor.
public class Example {
public static void main(String[] args) {
Result<String> successResult = Result.ok("Operação deu certo");
successResult.ifOkOrElse(
value -> System.out.println("Sucesso: " + value),
error -> System.err.println("Erro: " + error.getMessage())
);
}
}
Saída:
Sucesso: Operação deu certo
Exemplo 2: Lidando com uma Operação com Falha
Aqui, simulamos uma operação que deu errado e retorna um Result contendo um erro.
public class Example {
public static void main(String[] args) {
Result<String> errorResult = Result.error(new Exception("Operação falhou"));
errorResult.ifOkOrElse(
value -> System.out.println("Sucesso: " + value),
error -> System.err.println("Erro: " + error.getMessage())
);
}
}
Saída:
Erro: Operação falhou
Exemplo 3: Encadeando Operações com map
Esse exemplo mostra como transformar o valor dentro de um Result usando o método map.
public class Example {
public static void main(String[] args) {
Result<Integer> initialResult = Result.ok(5);
Result<Integer> transformedResult = initialResult.map(value -> value * 2);
transformedResult.ifOkOrElse(
value -> System.out.println("Valor Transformado: " + value),
error -> System.err.println("Erro: " + error.getMessage())
);
}
}
Saída:
Valor Transformado: 10
Exemplo 4: Encadeando Operações com flatMap
Nesse exemplo, a gente usa o método flatMap pra encadear operações que retornam instâncias de Result.
public class Example {
public static void main(String[] args) {
Result<Integer> initialResult = Result.ok(5);
Result<Integer> finalResult = initialResult.flatMap(value -> Result.ok(value * 2));
finalResult.ifOkOrElse(
value -> System.out.println("Valor Final: " + value),
error -> System.err.println("Erro: " + error.getMessage())
);
}
}
Saída:
Valor Final: 10
Esses exemplos mostram como o Monad Result pode ser usado pra lidar com operações que podem dar certo ou errado e como encadear operações de uma maneira funcional.