Translate

viernes, 30 de enero de 2026

HTTP en elm

 


A menudo resulta útil obtener información de otras fuentes de internet.

Por ejemplo, supongamos que queremos cargar el texto completo de "Opinión Pública" de Walter Lippmann. Publicado en 1922, este libro ofrece una perspectiva histórica sobre el auge de los medios de comunicación y sus implicaciones para la democracia. Para nuestros propósitos, nos centraremos en cómo usar el paquete elm/http para integrar este libro en nuestro programa.

Si queres ver el ejemplo en acción, hace click en este link: https://elm-lang.org/examples/book


import Browser

import Html exposing (Html, text, pre)

import Http


-- MAIN


main =

  Browser.element

    { init = init

    , update = update

    , subscriptions = subscriptions

    , view = view

    }


-- MODEL


type Model

  = Failure

  | Loading

  | Success String


init : () -> (Model, Cmd Msg)

init _ =

  ( Loading

  , Http.get

      { url = "https://elm-lang.org/assets/public-opinion.txt"

      , expect = Http.expectString GotText

      }

  )


-- UPDATE


type Msg

  = GotText (Result Http.Error String)


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    GotText result ->

      case result of

        Ok fullText ->

          (Success fullText, Cmd.none)


        Err _ ->

          (Failure, Cmd.none)


-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg

subscriptions model =

  Sub.none


-- VIEW


view : Model -> Html Msg

view model =

  case model of

    Failure ->

      text "I was unable to load your book."


    Loading ->

      text "Loading..."


    Success fullText ->

      pre [] [ text fullText ]


Algunas partes de esto deberían resultar familiares de ejemplos anteriores de la arquitectura Elm. Seguimos teniendo un modelo de nuestra aplicación. Seguimos teniendo una actualización que reacciona a los mensajes. Seguimos teniendo una función de vista que muestra todo en pantalla.

Las nuevas partes amplían el patrón principal que vimos antes con algunos cambios en init y update, y la incorporación de la suscripción.

La función init describe cómo inicializar nuestro programa:


init : () -> (Model, Cmd Msg)

init _ =

  ( Loading

  , Http.get

      { url = "https://elm-lang.org/assets/public-opinion.txt"

      , expect = Http.expectString GotText

      }

  )

Como siempre, debemos generar el modelo inicial, pero ahora también generamos un comando que indica lo que queremos hacer inmediatamente. Ese comando generará un mensaje que se introduce en la función de actualización.

Nuestro sitio web del libro se inicia en estado de carga y queremos obtener el texto completo. Al realizar una solicitud GET con Http.get, especificamos la URL de los datos que queremos obtener y qué datos esperamos que contengan. En nuestro caso, la URL apunta a datos del sitio web de Elm y esperamos que sea una cadena grande que podamos mostrar en pantalla.

La línea Http.expectString GotText indica algo más que esperamos una cadena. También indica que, cuando recibamos una respuesta, esta debe convertirse en un mensaje GotText:


type Msg

  = GotText (Result Http.Error String)


-- GotText (Ok "The Project Gutenberg EBook of ...")

-- GotText (Err Http.NetworkError)

-- GotText (Err (Http.BadStatus 404))


Tengamos en cuenta que usamos el tipo Result, mencionado en un par de secciones anteriores. Esto nos permite considerar completamente los posibles fallos en nuestra función de actualización. Hablando de funciones de actualización...

Nuestra función de actualización también devuelve un poco más de información:


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    GotText result ->

      case result of

        Ok fullText ->

          (Success fullText, Cmd.none)


        Err _ ->

          (Failure, Cmd.none)


Al observar la firma de tipo, vemos que no solo devolvemos un modelo actualizado. También generamos un comando que indica lo que queremos que haga Elm.

Pasando a la implementación, realizamos la coincidencia de patrones con los mensajes de forma normal. Cuando llega un mensaje GotText, inspeccionamos el resultado de nuestra solicitud HTTP y actualizamos nuestro modelo según si fue un éxito o un fracaso. La novedad es que también proporcionamos un comando.

En caso de obtener el texto completo correctamente, ejecutamos Cmd.none para indicar que no hay más trabajo que hacer. ¡Ya obtuvimos el texto completo!

Y en caso de algún error, también ejecutamos Cmd.none y simplemente nos rendimos. El texto del libro no se cargó. Si quisiéramos ser más sofisticados, podríamos realizar la coincidencia de patrones con Http.Error y reintentar la solicitud si se agota el tiempo de espera o algo similar.

La cuestión es que, independientemente de cómo decidamos actualizar nuestro modelo, también podemos ejecutar nuevos comandos. ¡Necesito más datos! ¡Quiero un número aleatorio! Etc.

La otra novedad de este programa es la función de suscripción. Permite consultar el modelo y decidir si se desea suscribirse a cierta información. En nuestro ejemplo, usamos Sub.none para indicar que no necesitamos suscribirnos a nada, pero pronto veremos un ejemplo de un reloj al que queremos suscribirnos a la hora actual.

Al crear un programa con Browser.element, configuramos un sistema como este:

Podemos ejecutar comandos desde init y update. Esto nos permite, por ejemplo, realizar solicitudes HTTP cuando queramos. También podemos suscribirnos a información interesante. (¡Veremos un ejemplo de suscripciones más adelante!)


martes, 27 de enero de 2026

Teorema de Brewer (CAP)


El Teorema de Brewer fue propuesto por Eric Brewer en el año 2000 y demostrado formalmente por Seth Gilbert y Nancy Lynch en 2002.

Establece que en un sistema distribuido no es posible garantizar simultáneamente las tres propiedades siguientes:

  • C — Consistency (Consistencia)

  • A — Availability (Disponibilidad)

  • P — Partition Tolerance (Tolerancia a particiones)

📘 En otras palabras:

Un sistema distribuido solo puede cumplir dos de las tres propiedades al mismo tiempo.


🔹 Las tres propiedades explicadas

1. Consistency (Consistencia)

Todos los nodos del sistema ven los mismos datos al mismo tiempo.
Después de una escritura exitosa, cualquier lectura posterior devuelve el valor más reciente.

📘 Ejemplo:
Si un usuario actualiza su saldo bancario en un nodo, otro usuario que consulta desde otro nodo ve inmediatamente el saldo actualizado.

💡 Equivale a la consistencia de una base de datos relacional tradicional.


2. Availability (Disponibilidad)

El sistema responde a todas las solicitudes, incluso si hay fallos en algunos nodos.
Cada petición recibe una respuesta válida —aunque los datos no siempre sean los más recientes.

📘 Ejemplo:
Un servidor cae, pero otros nodos siguen respondiendo a las consultas, aunque con datos levemente desactualizados.

💡 Se prioriza que el sistema siga funcionando sobre la precisión inmediata de los datos.


3. Partition Tolerance (Tolerancia a Particiones)

El sistema sigue funcionando a pesar de fallos en la comunicación entre nodos.
Una “partición” ocurre cuando los nodos se dividen en grupos que no pueden comunicarse entre sí.

📘 Ejemplo:
Si un centro de datos se desconecta de otro, ambos grupos de nodos continúan procesando solicitudes de sus usuarios locales.

💡 En sistemas distribuidos reales, las particiones son inevitables, por lo tanto P siempre debe cumplirse.


🔹 Las combinaciones posibles (el triángulo CAP)

Dado que P es ineludible en sistemas distribuidos, los diseñadores deben elegir entre priorizar Consistencia (C) o Disponibilidad (A) durante una partición.

Tipo de sistemaPropiedadesDescripciónEjemplo
CP (Consistent + Partition Tolerant)Garantiza consistencia, pero puede sacrificar disponibilidad.Prefiere bloquear operaciones antes que devolver datos desactualizados.HBase, MongoDB (modo strong), Google Spanner
AP (Available + Partition Tolerant)Garantiza disponibilidad, aunque puede devolver datos antiguos.Permite leer/escribir aunque haya inconsistencias temporales.Cassandra, DynamoDB, CouchDB
CA (Consistent + Available)Teóricamente consistente y disponible, pero no tolera particiones (solo en sistemas centralizados).Solo posible en bases locales o de un solo nodo.PostgreSQL, MySQL en una sola máquina

🔹 Ejemplo práctico

Imaginemos un sistema de ventas distribuido entre dos centros de datos:

  • Un cliente compra un producto en nodo A.

  • En ese momento, la red se corta (partición).

  • Otro cliente consulta el stock en nodo B.

Decisión del sistema:

  • Si prioriza Consistencia (CP) → bloquea la consulta hasta restablecer la conexión (no hay riesgo de datos incorrectos, pero hay demoras).

  • Si prioriza Disponibilidad (AP) → responde con el stock viejo (el sistema sigue activo, pero los datos pueden ser inconsistentes).


🔹 Implicancias del teorema CAP

  • No hay una solución única “mejor”: depende del tipo de aplicación.

  • En la práctica, se buscan equilibrios dinámicos (por ejemplo, consistencia eventual).

  • Los sistemas modernos (como los basados en microservicios y nube) tienden hacia AP con mecanismos de sincronización posterior.


🔸 Ejemplo de elección según el caso

Tipo de sistemaRequiereEjemplo de prioridad CAP
Banca / FinanzasPrecisión absolutaCP
Redes sociales / mensajeríaDisponibilidad continuaAP
ERP / e-commerce con stock centralizadoBalance intermedioCP o Eventual Consistency
Análisis de logs / métricasRendimiento sobre consistenciaAP

🔹 Consistencia eventual (Eventual Consistency)

Es un modelo intermedio que adoptan muchas bases de datos NoSQL distribuidas:

Los datos pueden estar temporalmente inconsistentes, pero eventualmente todos los nodos convergen al mismo estado.

📘 Ejemplo:
En DynamoDB o Cassandra, una actualización puede tardar unos segundos en propagarse, pero luego todos los nodos mostrarán el mismo valor.


🔹 CAP en bases de datos reales

Base de DatosTipoPropiedades según CAP
PostgreSQL / MySQL (1 nodo)RelacionalCA
MongoDB (replicación con write concern)NoSQLCP configurable
CassandraNoSQLAP
Amazon DynamoDBNoSQLAP (eventual o strong configurable)
Google SpannerSQL distribuidoCP (usa relojes atómicos para consistencia global)
Redis ClusterIn-memoryAP

🔹 Conclusión

El Teorema de Brewer (CAP) demuestra que no existe un sistema distribuido perfecto:

siempre hay que elegir entre consistencia y disponibilidad en presencia de particiones.

Los ingenieros deben decidir qué propiedad es más crítica según las necesidades del negocio:

  • CP: si la precisión de los datos es esencial.

  • AP: si la disponibilidad es más importante.

  • CA: solo en entornos centralizados o sin distribución.



PropiedadSignificadoCuando se prioriza
C – ConsistencyTodos los nodos ven el mismo dato al mismo tiempoFinanzas, stock crítico
A – AvailabilitySiempre responde, aunque los datos no sean los últimosRedes sociales, mensajería
P – Partition ToleranceTolera fallas de comunicaciónNecesario en sistemas distribuidos
Combinaciones posiblesCP, AP, CA (teórica)Elección según contexto


Rue: Un nuevo lenguaje de programación diseñado con ayuda de IA


Rue es un lenguaje de programación orientado a sistemas que pretende ofrecer:

  • Seguridad de memoria sin garbage collection (sin un recolector de basura tradicional).
  • Una experiencia de uso más accesible que Rust, pero manteniendo capacidades propias de lenguajes de bajo nivel.
  • Compilación nativa a código máquina
  • Sin máquina virtual ni dependencia de un runtime pesado. 


Aunque todavía está en una etapa temprana de desarrollo, Rue ya cuenta con un compilador funcional y soporte inicial para estructuras básicas del lenguaje.

Lo que hace particularmente interesante a Rue no es solo su propuesta técnica, sino cómo se está construyendo:

En lugar de trabajar con un gran equipo humano, Klabnik ha utilizado de forma intensiva el modelo de IA Claude de Anthropic para escribir buena parte del código del compilador.

Según reportes, más de 70 000 líneas de código Rust fueron generadas por Claude en apenas dos semanas de trabajo, bajo la supervisión y diseño de Klabnik. 

Este enfoque representa una nueva forma de colaboración hombre-IA en proyectos de infraestructura compleja, como lo es el desarrollo de compiladores y lenguajes de programación.


Rue parte de una pregunta central:

¿Qué pasa si no intentamos competir con C o C++ en rendimiento máximo, pero aún así mantenemos seguridad de memoria sin garbage collection, y simplificamos la experiencia del desarrollador?


Algunas ideas clave del diseño:

  • Elimina el borrow checker tradicional de Rust y usa conceptos como parámetros “inout” para manejar la propiedad temporal de datos. 
  • Simplifica ciertas restricciones de Rust para que el lenguaje sea más accesible a nuevos programadores.
  • Acepta que algunas capacidades (como iteradores que toman prestados datos del contenedor) no sean compatibles con las reglas de propiedad simplificadas. 
  • Este enfoque trae trade-offs claros: puede ser menos expresivo en ciertos patrones avanzados pero más fácil de entender al comenzar. 


Rue sigue en una fase muy temprana. Actualmente:

  • Tiene soporte básico de control de flujo y funciones.
  • El compilador genera ejecutables nativos sin GC.
  • Falta completar aspectos esenciales como:
    • Gestión de memoria completa (heap y estructuras complejas).
    • Soporte para herramientas del ecosistema (gestor de paquetes, servidor de lenguaje, etc.).
    • Modelos de concurrencia o librerías estándar maduras. 


El proyecto está disponible como código abierto en GitHub y también cuenta con un sitio web oficial donde se puede seguir su evolución.


Rue no está diseñado para reemplazar a Rust o a otros lenguajes de sistemas establecidos. En cambio, representa:

  • Un experimento de diseño: encontrar un equilibrio distinto entre seguridad, rendimiento y ergonomía.
  • Una exploración de colaboración humano-IA en ingeniería de software avanzada.
  • Un ejemplo de cómo los lenguajes de programación podrían construirse de manera más rápida con herramientas de IA de próxima generación. 


La comunidad ha reaccionado con interés y escepticismo, reconociendo el potencial, pero consciente de los desafíos que una propuesta así enfrenta antes de ser ampliamente adoptada. 


Dejo link: https://www.infoq.com/news/2026/01/steve-klabnik-rue-language-ai/ 

domingo, 25 de enero de 2026

Scala 3.8: Una evolución significativa del lenguaje


El 22 de enero de 2026 se anunció el lanzamiento de Scala 3.8, una versión que moderniza partes importantes del lenguaje y prepara el camino para Scala 3.9 LTS (soporte a largo plazo). Esta actualización trae cambios técnicos profundos, mejoras en la biblioteca estándar y estabilizaciones de características esperadas por la comunidad. 


Scala 3.8 requiere JDK 17 o posterior para compilar y ejecutar programas. Esto marca una ruptura con versiones anteriores que soportaban JDK 8, y responde a cambios en las futuras versiones de la JVM donde APIs internas como sun.misc.Unsafe ya no son accesibles por defecto. 


Hasta ahora la biblioteca estándar de Scala se compilaba con Scala 2.13 y se reutilizaba desde Scala 3 gracias a la compatibilidad binaria cuidada. En Scala 3.8 la biblioteca estándar ya se compila con Scala 3 nativamente.

Esto no solo moderniza su implementación, sino que sienta las bases para liberar la biblioteca estándar de las dependencias históricas de Scala 2 en versiones futuras. 


A partir de Scala 3.8 el REPL ya no se incluye directamente en la distribución principal del compilador. Ahora es un artefacto independiente que debe agregarse explícitamente si lo necesitás. 

Esto permite:

  • Reducir el tamaño base de Scala.
  • Integrar mejor el REPL en herramientas y entornos de desarrollo.
  • Mejorar la experiencia de uso con nuevas librerías como fansi y pprint, haciendo la salida más legible. 


Scala 3.8 estabiliza varias mejoras importantes al lenguaje que estaban en preview:

“Better Fors”:  La versión mejorada de las comprensiones for que se desugarizan de forma más eficiente y natural ya está activada por defecto. Esto hace al código más predecible y evita algunas operaciones innecesarias de map/flatMap. 


La nueva característica runtimeChecked (SIP-57) reemplaza el uso histórico de: @unchecked cuando queremos que ciertas verificaciones de patrón se hagan en tiempo de ejecución. Es más clara y usable en cadenas de operaciones que pueden lanzar excepciones por patrones no exhaustivos. 


Características experimentales:


Scala 3.8 también introduce varias mejoras en estado de preview, disponibles con el flag -preview:

  • Implicits con into: Permite permitir conversiones implícitas de forma más controlada usando la palabra clave into.
  • Igualdad estricta con patrones: Una novedad experimental que facilita el uso de pattern matching seguro bajo strictEquality.
  • Varargs flexibles: Soporte experimental para spread múltiple en argumentos, haciendo el uso de varargs más expresivo y menos limitado. 


Además hay cambios del compilador y la biblioteca estándar:

  • Recomendaciones en herramientas de construcción: SBT, Mill y Scala CLI necesitan versiones actualizadas para trabajar correctamente con Scala 3.8. 
  • IDEs como IntelliJ IDEA y Metals están adaptándose para dar soporte completo a las nuevas características y cambios en la librería. 


Scala 3.8 marca el final del ciclo de nuevas características antes de entrar en feature freeze para preparar la versión Scala 3.9 LTS, que se espera sea la próxima distribución con soporte a largo plazo. 

Esto significa que muchas de las funciones estabilizadas en 3.8 serán la base para la próxima versión LTS, sin cambios incompatibles mayores cuando se publique. 


Scala 3.8 representa un hito en la evolución del lenguaje:

  • Moderniza la biblioteca estándar.
  • Eleva el requisito de JDK 17+.
  • Mejora el desugaring y el runtimeChecked.
  • Separa el REPL como artefacto.
  • Estabiliza mejoras largamente esperadas.
  • Abre la puerta a nuevas características experimentales.


En resumen, es una versión que cambia la base técnica del lenguaje, prepara el terreno para el futuro y consolida Scala como una de las opciones más potentes en la JVM para programación moderna. 


Dejo link:  https://www.scala-lang.org/news/3.8/

API REST con Crystal, Kemal y SQLite


En este post vamos a crear una API REST usando el lenguaje Crystal, el microframework Kemal, y la base de datos SQLite, sin depender de ORMs.

El objetivo: lograr una API rápida, compilada a binario, y con sintaxis clara.

Asegurate de tener instalado:

  • Crystal
  • Shards (el gestor de dependencias)
  • SQLite3


Verificá con:

crystal --version

sqlite3 --version


Creamos el proyecto base:


crystal init app kemal_api

cd kemal_api


Editá el archivo shard.yml y agregá las dependencias:


dependencies:

  kemal:

    github: kemalcr/kemal

  sqlite3:

    github: crystal-lang/crystal-sqlite3


Instalá las dependencias:

shards install


Creamos una base de datos SQLite y una tabla simple:


sqlite3 data.db "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);"


A hacer las apis: 


require "kemal"

require "sqlite3"

require "json"


DB_URL = "sqlite3://./data.db"


# --- Funciones de acceso a la base de datos ---

def get_users

  users = [] of Hash(String, String)

  DB.open DB_URL do |db|

    db.query("SELECT id, name, email FROM users") do |rs|

      rs.each do

        users << {

          "id" => rs.read(Int32).to_s,

          "name" => rs.read(String),

          "email" => rs.read(String)

        }

      end

    end

  end

  users

end


def get_user(id : Int32)

  DB.open DB_URL do |db|

    db.query_one?("SELECT id, name, email FROM users WHERE id = ?", id, as: {Int32, String, String})

  end

end


def create_user(name : String, email : String)

  DB.open DB_URL do |db|

    db.exec("INSERT INTO users (name, email) VALUES (?, ?)", name, email)

  end

end


# --- Rutas Kemal ---


get "/users" do

  get_users.to_json

end


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

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

  if user = get_user(id)

    { id: user[0], name: user[1], email: user[2] }.to_json

  else

    env.response.status_code = 404

    { error: "User not found" }.to_json

  end

end


post "/users" do |env|

  data = JSON.parse(env.request.body.not_nil!)

  create_user(data["name"].as_s, data["email"].as_s)

  env.response.status_code = 201

  { message: "User created" }.to_json

end


Kemal.run



Ejecutá:

crystal run src/kemal_api.cr


La API se levanta en http://localhost:3000


Con Get

curl http://localhost:3000/users


Con Post

curl -X POST http://localhost:3000/users \

  -H "Content-Type: application/json" \

  -d '{"name": "Emanuel", "email": "ema@example.com"}'


Buscar un nuevo users es con id:

curl http://localhost:3000/users/1


Con Crystal + Kemal + SQLite podés construir APIs REST livianas, rápidas y auto-contenidas, ideales para proyectos pequeños o microservicios.

El código es legible, la ejecución veloz, y la experiencia de desarrollo tan fluida como en Ruby, pero con el rendimiento de C.


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.