Translate

jueves, 22 de enero de 2026

APIs Ligeras con Crystal y Kemal parte 2


Seguimos con Crystal + Kemal. Podemos simular una API para gestionar usuarios en memoria:


require "kemal"

require "json"


struct User

  property id : Int32

  property name : String

end


users = [

  User.new(id: 1, name: "Alice"),

  User.new(id: 2, name: "Bob")

]


get "/users" do

  users.to_json

end


get "/users/:id" do |env|

  id = env.params.url["id"].to_i

  user = users.find { |u| u.id == id }

  if user

    user.to_json

  else

    env.response.status_code = 404

    { error: "User not found" }.to_json

  end

end


Kemal.run


Con solo unas líneas, tenés una API REST rápida y compilada a código nativo.


Kemal permite agregar middlewares personalizados para logging, autenticación o cabeceras:


before_all do |env|

  puts "Request: #{env.request.method} #{env.request.path}"

end


También incluye un middleware logger integrado:


add_handler Kemal::Logger.new


Gracias al modelo de fibers de Crystal, Kemal puede manejar múltiples requests concurrentes sin bloquear el hilo principal.

Esto lo hace ideal para APIs I/O-bound (por ejemplo, servicios que consultan bases de datos o APIs externas).


get "/async" do

  spawn do

    sleep 1

    puts "Tarea asíncrona completada"

  end

  "Procesando..."

end


Kemal es un ejemplo perfecto del espíritu de Crystal:

  • Sintaxis clara y expresiva.
  • Ejecución compilada, rápida y eficiente.
  • Concurrencia simple y no bloqueante.
  • Framework minimalista y productivo.

Si te gusta Sinatra en Ruby o Express en Node.js, vas a sentirte como en casa, pero con el rendimiento de un lenguaje compilado.

miércoles, 21 de enero de 2026

APIs Ligeras con Crystal y Kemal


Crystal es un lenguaje que combina la velocidad de C con la elegancia de Ruby.

Entre sus frameworks web más populares se destaca Kemal, un microframework minimalista y rápido, muy similar a Sinatra (Ruby) o Express (Node.js).

Primero, asegúrate de tener instalado Crystal.

Podés verificarlo con:

crystal --version


Y si no lo tenés instalado podés ejecutar este comando: 

curl -fsSL https://crystal-lang.org/install.sh | sudo bash


Luego, instalá Kemal agregándolo a tu proyecto con shards, el gestor de dependencias de Crystal.


shards init


Editá el archivo shard.yml y agregá:


dependencies:

  kemal:

    github: kemalcr/kemal


Finalmente, instalá las dependencias:


shards install


Creamos un archivo app.cr:


require "kemal"


get "/" do

  "Hola desde Crystal con Kemal!"

end


Kemal.run


Y ejecutamos:

crystal run app.cr


📍 Luego, abrí http://localhost:3000

Vas a ver la respuesta:


Hola desde Crystal con Kemal!


Kemal permite definir rutas con parámetros y manejar JSON fácilmente:


require "kemal"

require "json"


get "/saludo/:nombre" do |env|

  nombre = env.params.url["nombre"]

  { mensaje: "Hola, #{nombre}!" }.to_json

end


Kemal.run


GET http://localhost:3000/saludo/Emanuel


Responde con:

{"mensaje": "Hola, Emanuel!"}


martes, 20 de enero de 2026

Concurrencia en Crystal: Fibers y Channels al Estilo Go


El lenguaje Crystal combina la sintaxis elegante de Ruby con un modelo de concurrencia ligero y eficiente, inspirado en Go.

En lugar de hilos del sistema operativo, Crystal usa fibers, que permiten ejecutar múltiples tareas de forma concurrente dentro del mismo proceso.

Una fiber es una unidad ligera de ejecución gestionada por el runtime de Crystal (no por el sistema operativo).

Varias fibers pueden ejecutarse “en paralelo” sobre un solo hilo de sistema, haciendo que la concurrencia sea cooperativa y eficiente.


Se crean usando la palabra clave spawn:


spawn do

  puts "Hola desde una fiber!"

end


puts "Hola desde el hilo principal!"

sleep 0.1


Salida posible:

Hola desde el hilo principal!

Hola desde una fiber!


El sleep al final evita que el programa termine antes de que la fiber se ejecute (las fibers corren de forma asíncrona).

Las fibers se comunican a través de channels, una abstracción segura para enviar y recibir mensajes sin necesidad de locks.


channel = Channel(String).new

spawn do

  channel.send("Mensaje desde otra fiber")

end


puts channel.receive


Salida:

Mensaje desde otra fiber


Los Channel son tipados (Channel(Int32), Channel(String), etc.), y pueden usarse para coordinar tareas concurrentes.


Veamos un ejemplo más realista con varias fibers:


channel = Channel(Int32).new


# Productor

spawn do

  5.times do |i|

    puts "Produciendo #{i}"

    channel.send(i)

    sleep 0.2

  end

  channel.close

end


# Consumidor

spawn do

  for value in channel

    puts "Consumiendo #{value}"

  end

end


sleep 2


Salida:

Produciendo 0

Consumiendo 0

Produciendo 1

Consumiendo 1

Produciendo 2

Consumiendo 2

Produciendo 3

Consumiendo 3

Produciendo 4

Consumiendo 4


Crystal no crea múltiples hilos del sistema por cada fiber.

Las fibers son gestionadas por el scheduler del runtime.

El modelo es asíncrono cooperativo: las fibers ceden el control cuando hacen operaciones de I/O o esperan datos.

Este enfoque reduce el costo de cambio de contexto y permite miles de fibers concurrentes sin overhead.

Crystal adopta el modelo CSP (Communicating Sequential Processes) de Go, pero mantiene la simplicidad y legibilidad de Ruby.

Es importante notar que Crystal 1.x usa un solo hilo del sistema (no hay paralelismo real entre núcleos).

Sin embargo, el equipo de Crystal está trabajando en soporte multithreaded para futuras versiones.


Esto significa que las fibers son ideales para:

  • I/O concurrente (HTTP, base de datos, archivos).
  • Operaciones asíncronas livianas.
  • Pero no para tareas intensivas en CPU.


La concurrencia en Crystal es una de sus características más elegantes:

  • Usa fibers para tareas concurrentes sin complicaciones.
  • Permite comunicación segura con channels.
  • Ofrece un modelo simple, escalable y eficiente.


Si disfrutás del enfoque de Go o Elixir, pero querés la sintaxis de Ruby y velocidad de C, Crystal es una alternativa brillante para explorar.

sábado, 17 de enero de 2026

Crystal: El Lenguaje de Programación que Combina la Elegancia de Ruby con la Velocidad de C


En el mundo de la programación, los lenguajes suelen ubicarse entre dos extremos: los rápidos y eficientes, como C o Rust, y los expresivos y productivos, como Ruby o Python.

Crystal intenta unir lo mejor de ambos mundos: la velocidad de C con la sintaxis elegante de Ruby.

Crystal es un lenguaje de programación compilado, tipado estáticamente, y con una sintaxis muy parecida a Ruby.

Está diseñado para ofrecer una experiencia de desarrollo rápida y agradable, sin sacrificar el rendimiento.


Algunos de sus pilares son:

  • Sintaxis legible y concisa.
  • Compilación nativa a binarios.
  • Tipado estático con inferencia de tipos.
  • Recolección de basura (GC).
  • Soporte para concurrencia mediante fibers y channels, inspirados en Go.


Un programa clásico en Crystal se ve así:



def greet(name : String)

  puts "Hola, #{name}!"

end


greet("Emanuel")


A simple vista, parece Ruby. Pero a diferencia de Ruby, Crystal compila a código máquina:


crystal build hello.cr

./hello

# => Hola, Emanuel!


Crystal detecta automáticamente los tipos sin necesidad de declararlos explícitamente, usa inferencia de tipos:


name = "Crystal"

version = 1.12

puts "#{name} #{version}"


El compilador infiere que name es String y version es Float64, verificando los tipos en tiempo de compilación.

Esto evita muchos errores sin perder flexibilidad.


Crystal implementa un modelo de concurrencia basado en fibers (hilos ligeros) y channels, similar a Go:


channel = Channel(Int32).new


spawn do

  3.times do |i|

    channel.send(i)

  end

end


3.times do

  puts "Recibido: #{channel.receive}"

end


Cada spawn ejecuta una tarea concurrente dentro del mismo proceso, permitiendo aplicaciones altamente escalables sin la complejidad de los hilos tradicionales.


Crystal incluye muchas herramientas integradas:

  • crystal build → compila el código a un ejecutable.
  • crystal run → ejecuta directamente un programa.
  • crystal spec → framework de pruebas (similar a RSpec).
  • shards → gestor de dependencias oficial.


Ejemplo de uso con Shards:


shards init

shards install


Crystal se utiliza en:

  • Desarrollo de APIs REST (con frameworks como Kemal o Lucky).
  • CLI tools y aplicaciones de sistema.
  • Programas que requieren rendimiento sin sacrificar legibilidad.


Ejemplo con Kemal (un microframework web):


require "kemal"


get "/" do

  "Hola desde Crystal!"

end


Kemal.run


Crystal es un lenguaje ideal si buscás:

  • La belleza sintáctica de Ruby.
  • El rendimiento de C.
  • Un sistema de tipos seguro pero sin verbosidad.
  • Concurrencia sencilla y eficiente.


Aunque su ecosistema es más pequeño que el de Go o Rust, Crystal está ganando tracción entre quienes valoran productividad y rendimiento equilibrados.



viernes, 9 de enero de 2026

Manejo Global de Excepciones con @ControllerAdvice en Spring Boot

En una aplicación Spring Boot, manejar errores de forma consistente puede volverse complicado cuando tenemos muchos controladores. Para evitar repetir lógica de manejo de excepciones en cada uno, Spring nos ofrece una poderosa anotación: @ControllerAdvice.

@ControllerAdvice es una anotación que permite manejar excepciones globalmente en todos los controladores (@Controller o @RestController).

Funciona como un interceptor de excepciones lanzadas por los controladores y te permite centralizar la lógica de manejo de errores.

Se usa comúnmente junto con @ExceptionHandler para capturar tipos específicos de excepciones.

Supongamos que tenemos un controlador que lanza una excepción cuando un recurso no se encuentra:


@RestController

@RequestMapping("/api/users")

public class UserController {


    @GetMapping("/{id}")

    public User getUser(@PathVariable Long id) {

        if (id == 1) {

            return new User(1L, "Alice");

        }

        throw new UserNotFoundException("User not found with id: " + id);

    }

}


Y defines la excepción personalizada:


public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {

        super(message);

    }

}


Ahora puedes manejar esta excepción de forma global con @ControllerAdvice:


@ControllerAdvice

public class GlobalExceptionHandler {


    @ExceptionHandler(UserNotFoundException.class)

    public ResponseEntity<Map<String, String>> handleUserNotFound(UserNotFoundException ex) {

        Map<String, String> response = new HashMap<>();

        response.put("error", ex.getMessage());

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);

    }


    @ExceptionHandler(Exception.class)

    public ResponseEntity<Map<String, String>> handleGeneral(Exception ex) {

        Map<String, String> response = new HashMap<>();

        response.put("error", "Internal Server Error");

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);

    }

}


@ControllerAdvice se aplica a todos los controladores por defecto, pero también puede limitarse a un paquete o anotación específica:

@ControllerAdvice(basePackages = "com.example.api")


@ExceptionHandler indica qué tipo de excepción manejar.

Los métodos pueden devolver cualquier tipo de objeto compatible con Spring MVC: ResponseEntity, ModelAndView, o incluso un String.


Entre las ventajas tenemos: 

  • Centraliza el manejo de errores.
  • Evita duplicación de código en controladores.
  • Facilita la personalización de las respuestas HTTP.
  • Permite diferenciar entre distintos tipos de errores.


Si tu aplicación es una API REST, puedes usar: @RestControllerAdvice

Esta versión combina @ControllerAdvice con @ResponseBody, lo que simplifica el retorno de respuestas JSON automáticamente (sin necesidad de ResponseEntity explícito).

Veamos un ejemplo:


@RestControllerAdvice

public class ApiExceptionHandler {


    @ExceptionHandler(UserNotFoundException.class)

    public Map<String, String> handleUserNotFound(UserNotFoundException ex) {

        return Map.of("error", ex.getMessage());

    }

}


@ControllerAdvice es una herramienta esencial para crear APIs limpias, consistentes y fáciles de mantener en Spring Boot.

Al centralizar el manejo de excepciones, podés mejorar la legibilidad del código y ofrecer respuestas coherentes a los clientes.


jueves, 8 de enero de 2026

Métodos Monádicos en Rust — Parte 2


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.


lunes, 5 de enero de 2026

Métodos Monádicos en Rust

Rust no tiene palabras como mónada en su sintaxis, pero su sistema de tipos y métodos encadenados son monádicos por naturaleza.

Gracias a eso, se pueden escribir programas expresivos y seguros, sin null, sin excepciones y sin estructuras de control complejas.

Una mónada representa un valor junto con un contexto (por ejemplo, “puede faltar” o “puede fallar”).

Provee operaciones para:

  • Aplicar una función al valor si está presente (map).
  • Encadenar funciones que también devuelven valores en contexto (and_then).
  • Definir valores alternativos (or, or_else).


En Rust, eso se aplica directamente a Option y Result.

Option<T> representa un valor que puede existir (Some) o no (None).


let x: Option<i32> = Some(5);

let y = x.map(|n| n * 2).and_then(|n| Some(n + 3));


println!("{:?}", y); // Some(13)


Los metodos que tenemos son : 

  • map(f): Aplica `f` al valor si existe
  • and_then(f): Encadena funciones que devuelven Option
  • or(opt): Usa otro valor si está vacío 
  • or_else(f): Calcula alternativa solo si es None


Ejemplo:


let res = None.or(Some(10)).map(|x| x * 2);

println!("{:?}", res); // Some(20)


Result modela operaciones que pueden tener éxito (Ok) o error (Err), sin excepciones.


fn parse_number(s: &str) -> Result<i32, String> {

    s.parse::<i32>().map_err(|_| "Invalid number".to_string())

}


Encadenamiento monádico:


let result = parse_number("42")

    .map(|x| x * 2)

    .and_then(|x| if x > 50 { Ok(x) } else { Err("too small") })

    .or_else(|_| Ok(0));


println!("{:?}", result); // Ok(84)


Si una función devuelve Err, el resto de la cadena se saltea automáticamente.

No hay try/catch, ni comprobaciones manuales.

Rust también permite componer funciones monádicas de forma declarativa:


fn half(n: i32) -> Option<i32> {

    if n % 2 == 0 { Some(n / 2) } else { None }

}


fn add_ten(n: i32) -> Option<i32> {

    Some(n + 10)

}


let result = Some(8)

    .and_then(half)

    .and_then(add_ten);


println!("{:?}", result); // Some(14)


Si en algún punto hay None, toda la cadena devuelve None.


El operador ?desempaqueta automáticamente Result o Option y corta la ejecución si no hay valor.


fn process() -> Result<i32, String> {

    let x = parse_number("10")?; // si falla, retorna Err automáticamente

    let y = parse_number("20")?;

    Ok(x + y)

}


Internamente, ? usa and_then y map, pero con una sintaxis más natural.

Rust no necesita hablar de mónadas — las usa en cada línea de código.



viernes, 2 de enero de 2026

Métodos Monádicos en Go — Parte 2


En el post anterior definimos Result[T], un tipo genérico para representar operaciones que pueden fallar, junto con las funciones Map, FlatMap y OrElse.

Ahora vamos a llevar ese patrón al mundo concurrente, donde los errores y la sincronización suelen volverse un caos si no se estructuran bien.

Go facilita lanzar tareas concurrentes con goroutines, pero mezclar concurrencia y manejo de errores puede ser tedioso:


go func() {

    v, err := doSomething()

    if err != nil {

        // manejar error...

    }

    res, err := doSomethingElse(v)

    if err != nil {

        // otro error...

    }

    // ...

}()


Cada paso necesita su if err != nil, y los canales deben sincronizar resultados y errores manualmente.

Podemos crear una versión concurrente del patrón Result, donde cada operación devuelve un canal que emite un Result[T].


func Async[T any](f func() Result[T]) <-chan Result[T] {

    ch := make(chan Result[T], 1)

    go func() {

        defer close(ch)

        ch <- f()

    }()

    return ch

}


Creamos versiones asíncronas de Map y FlatMap:


func MapAsync[A, B any](in <-chan Result[A], f func(A) B) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        ch <- Map(r, f)

    }()

    return ch

}


func FlatMapAsync[A, B any](in <-chan Result[A], f func(A) <-chan Result[B]) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        if r.Err != nil {

            ch <- Err[B](r.Err)

            return

        }

        out := f(r.Value)

        ch <- <-out

    }()

    return ch

}


Supongamos que tenemos operaciones concurrentes que pueden fallar:


func FetchData() Result[int] {

    time.Sleep(time.Millisecond * 100)

    return Ok(10)

}


func Compute(x int) Result[int] {

    return Ok(x * 2)

}


func SaveResult(x int) Result[string] {

    if x > 15 {

        return Ok(fmt.Sprintf("Saved %d", x))

    }

    return Err[string](fmt.Errorf("too small"))

}


Podemos encadenarlas de forma limpia:


result := FlatMapAsync(

    MapAsync(Async(FetchData), Compute),

    func(x int) <-chan Result[string] { return Async(func() Result[string] { return SaveResult(x) }) },

)


fmt.Println(OrElse(<-result, "failed")) // "Saved 20"

Las ventajas son :

  • Las tareas se ejecutan en goroutines separadas.
  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • No hay if err != nil, ni sincronización manual.


Podés crear fácilmente un combinador All para ejecutar varias tareas concurrentes que devuelvan Result:


func All[T any](tasks ...func() Result[T]) Result[[]T] {

    ch := make(chan Result[T], len(tasks))

    for _, f := range tasks {

        go func(fn func() Result[T]) { ch <- fn() }(f)

    }


    var results []T

    for i := 0; i < len(tasks); i++ {

        r := <-ch

        if r.Err != nil {

            return Err[[]T](r.Err)

        }

        results = append(results, r.Value)

    }

    return Ok(results)

}


Si alguna tarea falla, el error se devuelve inmediatamente.

Si todas tienen éxito, se devuelven los resultados combinados.


El enfoque monádico aplicado a Go te permite combinar funciones puras y goroutines sin perder control del flujo ni del manejo de errores.

Aporta claridad a sistemas concurrentes y simplifica el código de coordinación.

Métodos Monádicos en Go


Go no usa palabras como mónada, pero su estilo basado en funciones y retorno explícito de errores se adapta perfectamente a la idea de encadenar operaciones que pueden fallar, sin perder claridad.

Con los genéricos (desde Go 1.18), podemos escribir funciones reutilizables que se comportan como map, flatMap, orElse de otros lenguajes.


El estilo Go para manejar errores:


value, err := DoSomething()

if err != nil {

    return 0, err

}

result, err := DoSomethingElse(value)

if err != nil {

    return 0, err

}

return result, nil


Funciona bien, pero escala mal cuando hay muchos pasos encadenados.

Podemos mejorarlo aplicando ideas monádicas.

Vamos a representar una operación que puede tener éxito o error:


type Result[T any] struct {

    Value T

    Err   error

}


Y creamos constructores simples:


func Ok[T any](v T) Result[T]   { return Result[T]{Value: v} }

func Err[T any](e error) Result[T] { return Result[T]{Err: e} }


Aplica una función al valor, si no hay error.


func Map[A, B any](r Result[A], f func(A) B) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return Ok(f(r.Value))

}


Aplica una función que también devuelve un Result, y lo aplana.


func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return f(r.Value)

}


OrElse: Devuelve un valor alternativo si hubo error.


func OrElse[T any](r Result[T], fallback T) T {

    if r.Err != nil {

        return fallback

    }

    return r.Value

}


Y como lo usamos? 


func ParseInt(s string) Result[int] {

    n, err := strconv.Atoi(s)

    if err != nil {

        return Err[int](err)

    }

    return Ok(n)

}


func DivideByTwo(n int) Result[int] {

    if n%2 != 0 {

        return Err[int](fmt.Errorf("odd number"))

    }

    return Ok(n / 2)

}


func main() {

    result := FlatMap(ParseInt("42"),

        func(n int) Result[int] {

            return Map(DivideByTwo(n), func(x int) int { return x * 3 })

        })


    fmt.Println(OrElse(result, 0)) // 63

}

¿Cuáles son las ventajas?

  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • Sin if err != nil en cada línea.
  • Código más funcional y declarativo.


Aunque Go no tenga sintaxis monádica ni azúcar funcional, su modelo basado en valores de retorno y funciones puras encaja perfectamente con la idea.

Con un par de funciones genéricas, podés escribir código más declarativo y mantenible.


jueves, 1 de enero de 2026

Métodos Monádicos en C# parte 3


En el post anterior, implementamos una clase Result<T> con métodos monádicos (Map, FlatMap, OrElse).

Ahora combinaremos esa idea con Task<T> para obtener un flujo asíncrono y seguro ante errores, sin try/catch y sin anidar await.

Supongamos que tenemos funciones asíncronas que pueden fallar:


Task<Result<User>> GetUserAsync(int id);

Task<Result<Order>> GetOrderAsync(User user);

Task<Result<Invoice>> CreateInvoiceAsync(Order order);


Queremos encadenarlas de forma limpia, propagando el error automáticamente, sin esto 👇:


var userResult = await GetUserAsync(id);

if (userResult is ErrorResult<User>) return userResult;


var orderResult = await GetOrderAsync(userResult.Value);

// etc...


Creamos extensiones que aplanan el contexto doble (Task<Result<T>>):


public static class TaskResultExtensions

{

    public static async Task<Result<U>> Map<T, U>(

        this Task<Result<T>> task, Func<T, U> f)

    {

        var result = await task;

        return result is OkResult<T> ok ? Result<U>.Ok(f(ok.Value)) 

                                        : Result<U>.Error(((ErrorResult<T>)result).Message);

    }


    public static async Task<Result<U>> FlatMap<T, U>(

        this Task<Result<T>> task, Func<T, Task<Result<U>>> f)

    {

        var result = await task;

        return result is OkResult<T> ok ? await f(ok.Value) 

                                        : Result<U>.Error(((ErrorResult<T>)result).Message);

    }

}


Veamos como usarlo: 


var invoice = await GetUserAsync(10)

    .FlatMap(GetOrderAsync)

    .FlatMap(CreateInvoiceAsync)

    .Map(invoice => invoice.WithDiscount(10))

    .FlatMap(SaveInvoiceAsync)

    .OrElse(Result<Invoice>.Error("No se pudo generar factura"));


¿Cuál es la ventaja?

  • Si cualquier paso falla, la cadena se corta automáticamente
  • No se necesitan try/catch ni comprobaciones manuales
  • El código se lee de arriba a abajo, como una secuencia lógica de pasos


Veamos otro ejemplo:


public async Task<Result<string>> GenerateInvoice(int userId)

{

    return await GetUserAsync(userId)

        .FlatMap(GetOrderAsync)

        .FlatMap(CreateInvoiceAsync)

        .Map(invoice => invoice.Id.ToString());

}


Si alguna función devuelve un Result.Error, ese error se propaga sin ejecutar los pasos siguientes.

El resultado final es un Result<string> con éxito o mensaje de error.