En lenguajes funcionales (y en C++23 con std::expected), existe un tipo que representa éxito o error sin excepciones.
En Java podemos implementarlo fácilmente y hacerlo monádico.
Result<T, E> encapsula dos posibles estados:
- Ok(T) → el cálculo fue exitoso
- Err(E) → ocurrió un error
Así evitamos lanzar excepciones, y podemos encadenar operaciones de manera declarativa.
public sealed interface Result<T, E> permits Ok, Err {
boolean isOk();
boolean isErr();
<U> Result<U, E> map(Function<? super T, ? extends U> f);
<U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f);
T orElse(T fallback);
}
public record Ok<T, E>(T value) implements Result<T, E> {
public boolean isOk() { return true; }
public boolean isErr() { return false; }
public <U> Result<U, E> map(Function<? super T, ? extends U> f) {
return new Ok<>(f.apply(value));
}
public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f) {
return f.apply(value);
}
public T orElse(T fallback) { return value; }
}
public record Err<T, E>(E error) implements Result<T, E> {
public boolean isOk() { return false; }
public boolean isErr() { return true; }
public <U> Result<U, E> map(Function<? super T, ? extends U> f) {
return new Err<>(error);
}
public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f) {
return new Err<>(error);
}
public T orElse(T fallback) { return fallback; }
}
Podemos usarlo, de esta manera:
Result<Integer, String> parseInt(String s) {
try {
return new Ok<>(Integer.parseInt(s));
} catch (NumberFormatException e) {
return new Err<>("Not a number");
}
}
Result<Integer, String> divideByTwo(int n) {
return (n % 2 == 0)
? new Ok<>(n / 2)
: new Err<>("Odd number");
}
var result = parseInt("42")
.flatMap(this::divideByTwo)
.map(x -> x * 3)
.orElse(0);
System.out.println(result); // 63
Si en cualquier paso se produce un error (Err), las transformaciones se detienen automáticamente.
No hay try/catch, ni comprobaciones manuales de estado.
Ventajas del enfoque monádico
- Evita excepciones y null.
- Encadena operaciones de manera natural.
- Explicita el flujo de éxito/error.
- Es fácilmente testeable y composable.
En Java no hace falta un lenguaje funcional completo para pensar funcionalmente.
Con un poco de sintaxis moderna (record, sealed interface, lambdas) podés crear tus propios tipos monádicos.
