Rust nos da Option y Result, pero podemos definir nuestra propia estructura monádica para aprender cómo se implementan los métodos map, and_then, y or_else.
Lo haremos con un tipo Maybe<T>, que representa un valor que puede o no existir (una versión casera de Option<T>).
#[derive(Debug, Clone, PartialEq)]
enum Maybe<T> {
Just(T),
Nothing,
}
- Just(T) representa un valor presente.
- Nothing representa ausencia de valor.
Implementemos los métodos monádicos
impl<T> Maybe<T> {
/// Aplica una función al valor si existe
fn map<U, F>(self, f: F) -> Maybe<U>
where
F: FnOnce(T) -> U,
{
match self {
Maybe::Just(v) => Maybe::Just(f(v)),
Maybe::Nothing => Maybe::Nothing,
}
}
/// Encadena funciones que devuelven Maybe
fn and_then<U, F>(self, f: F) -> Maybe<U>
where
F: FnOnce(T) -> Maybe<U>,
{
match self {
Maybe::Just(v) => f(v),
Maybe::Nothing => Maybe::Nothing,
}
}
/// Valor alternativo si es Nothing
fn or_else<F>(self, f: F) -> Maybe<T>
where
F: FnOnce() -> Maybe<T>,
{
match self {
Maybe::Just(_) => self,
Maybe::Nothing => f(),
}
}
}
Veamos como podemos usarlo:
fn half(n: i32) -> Maybe<i32> {
if n % 2 == 0 {
Maybe::Just(n / 2)
} else {
Maybe::Nothing
}
}
fn add_ten(n: i32) -> Maybe<i32> {
Maybe::Just(n + 10)
}
fn main() {
let result = Maybe::Just(8)
.and_then(half)
.and_then(add_ten)
.or_else(|| Maybe::Just(0));
println!("{:?}", result); // Just(14)
}
- Si algún paso devuelve Nothing, el resto se saltea.
- No hay if, ni unwrap, ni panic!.
También podés transformar el tipo interno con map:
let maybe_str = Maybe::Just(21)
.map(|x| format!("Value: {}", x))
.or_else(|| Maybe::Just("Fallback".to_string()));
println!("{:?}", maybe_str); // Just("Value: 21")
Podés generalizar este patrón para representar éxito o error, al estilo Result:
#[derive(Debug)]
enum Either<L, R> {
Left(L),
Right(R),
}
impl<L, R> Either<L, R> {
fn map<U, F>(self, f: F) -> Either<L, U>
where
F: FnOnce(R) -> U,
{
match self {
Either::Right(v) => Either::Right(f(v)),
Either::Left(e) => Either::Left(e),
}
}
fn and_then<U, F>(self, f: F) -> Either<L, U>
where
F: FnOnce(R) -> Either<L, U>,
{
match self {
Either::Right(v) => f(v),
Either::Left(e) => Either::Left(e),
}
}
}
Esto es básicamente lo que Result<T, E> hace internamente.
Las mónadas en Rust no son magia: son simplemente enums con funciones que saben cuándo continuar y cuándo parar.
