Translate

sábado, 27 de diciembre de 2025

Implementando nuestra propia mónada en Java


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.