Javier Valencia Javier Valencia
Gema facetada en rojo rubí junto al número de versión 3.0, portada de la serie de novedades de Ruby

Novedades de Ruby 3.0: la promesa del 3x3

Javier Valencia · · 5 min de lectura · 2 visitas · Desarrollo
ruby desarrollo programacion series tutorial

Arranca la serie Novedades de Ruby: un recorrido versión a versión por cada minor de Ruby desde la 3.0, contado por alguien que las fue estrenando en proyectos Rails reales. Un minor por entrega, hasta llegar a Ruby 4. La idea no es copiar el changelog —para eso ya están las release notes—, sino contar qué importó de verdad en el día a día y qué se quedó en anécdota. Y como escribo en 2026, me permito el lujo de la retrospectiva: ya sabemos qué envejeció bien.

El 25 de diciembre de 2020 —Matz lleva toda la vida publicando las versiones nuevas el día de Navidad, como quien deja un regalo bajo el árbol— salió Ruby 3.0. No era una versión más: era el cierre de una promesa que llevaba un lustro sobre la mesa, la de Ruby 3x3: "Ruby 3 será tres veces más rápido que Ruby 2".

El 3x3: cumplido con letra pequeña

¿Se cumplió? Sí en el benchmark, no en tu aplicación Rails. La medición oficial se hizo con Optcarrot, un emulador de NES escrito en Ruby, y ahí la 3.0 efectivamente triplicaba a Ruby 2.0. Buena parte del mérito era de MJIT, el compilador JIT que había llegado en la 2.6. Pero las propias release notes lo admitían con una honestidad que agradezco: MJIT ayudaba en cargas que machacan unos pocos métodos millones de veces —juegos, IA—, pero "todavía no está listo para optimizar cargas como Rails".

En mi día a día de entonces, actualizar a 3.0 no movió una décima los tiempos de respuesta. La lección, vista desde 2026, es que el 3x3 no fue un resultado sino una dirección: obligó al core team a tomarse el rendimiento en serio, y eso fructificó después (spoiler de la próxima entrega: se llama YJIT).

Ractor: paralelismo real, por fin (en teoría)

La novedad más ambiciosa fue Ractor, un modelo de actores que permite ejecutar código Ruby en paralelo de verdad, esquivando el GVL. El truco: los ractors no comparten objetos mutables; se comunican pasándose mensajes.

r = Ractor.new do
  msg = Ractor.receive
  msg.upcase
end

r.send("hola desde el hilo principal")
r.take # => "HOLA DESDE EL HILO PRINCIPAL"

Llegó marcado como experimental, con aviso explícito de que la especificación podía cambiar. Y aquí toca ser honesto: en 2026, Ractor sigue siendo una anécdota en el mundo Rails. Casi ninguna gema del ecosistema es ractor-safe, Rails no lo usa, y los pocos casos de éxito que conozco son herramientas de proceso puro. Fue un movimiento valiente y necesario a nivel de lenguaje, pero cinco años después sigo sin haberlo desplegado en producción ni una vez.

Fiber Scheduler: la puerta que abrió el async

Mucho más discreto, y a la larga más fértil, fue el Fiber Scheduler. Ruby 3.0 no trajo concurrencia asíncrona hecha; trajo el enchufe: una interfaz que intercepta operaciones bloqueantes (IO, Kernel#sleep, Thread#join, Mutex, Queue, Process.wait...) y se las cede a un scheduler que tú eliges. El intérprete no incluye ninguno por defecto.

Ese diseño —el lenguaje pone el gancho, la comunidad pone la implementación— es la razón de que hoy exista un ecosistema async maduro, con Falcon como servidor y gemas que hacen I/O concurrente sin cambiar tu código, sin await por todas partes. Sigue siendo un nicho comparado con el clásico Puma + threads, pero es un nicho sano y creciente. De todo lo experimental de la 3.0, esto es lo que mejor ha envejecido.

Pattern matching: de juguete a herramienta

El case/in había llegado experimental en la 2.7; en la 3.0 dejó de ser experimental y se le añadieron tres piezas:

  • La asignación "hacia la derecha" con =>, que desestructura sin ceremonia.
  • in como expresión booleana: devuelve true/false en lugar de lanzar excepción.
  • El find pattern (este sí, aún experimental): buscar un elemento en medio de una secuencia.
config = { db: { adapter: "postgresql", pool: 5 } }

case config
in { db: { adapter: String => adapter, pool: Integer => pool } }
  puts "#{adapter} con pool de #{pool}"
end

# Asignación hacia la derecha:
config => { db: { adapter: } }
adapter # => "postgresql"

# Find pattern: pesca el entero donde caiga
["GET", "/posts", 200, "OK"] => [*, Integer => status, *]
status # => 200

Confieso que en 2020 lo miré con escepticismo: me parecía Elixir con disfraz. Me equivoqué. Hoy es la feature de la 3.0 que más uso: para validar payloads de webhooks, para desmontar respuestas JSON de APIs, para los case que antes eran escaleras de if con dig. Ganó despacio y sin ruido, que es como ganan las buenas features.

RBS y TypeProf: el tipado que no caló (en mi mundo)

Ruby 3.0 estrenó la infraestructura oficial de tipos: RBS, un lenguaje para describir las firmas en ficheros .rbs separados del código, y TypeProf, un inferidor experimental que genera esas firmas leyendo Ruby sin anotar.

# user.rbs
class User
  attr_reader name: String
  def initialize: (name: String) -> void
  def admin?: () -> bool
end

La apuesta era elegante —tu .rb queda limpio, los tipos viven aparte—, pero cinco años después toca admitirlo: en los equipos Rails que conozco, el tipado gradual no ha calado. Quien lo necesitaba de verdad ya estaba en Sorbet (Shopify, Stripe), y el resto seguimos confiando en los tests y en la duck typing de toda la vida. RBS vive, alimenta el tooling de los editores por debajo, pero no cambió cómo escribimos Ruby.

Keyword arguments: el cambio que sí nos tocó a todos

Y luego estaba el trabajo sucio: la separación definitiva de los keyword arguments de los posicionales. Lo que en 2.7 era un warning, en 3.0 pasó a romper:

def create_user(name, admin: false)
  # ...
end

opts = { admin: true }
create_user("Javier", opts)   # ArgumentError en 3.0
create_user("Javier", **opts) # correcto

Esta fue la migración de verdad. Nada de jugar con actores: semanas de perseguir ArgumentError en gemas sin mantener y en código propio de 2014 que pasaba hashes alegremente. Fue doloroso y fue lo correcto: se acabaron los casos esquina absurdos de "¿esto es un hash o son keywords?". De los cambios de la 3.0, este es el que más facturas pasó y el que menos hemos vuelto a recordar, que es la mejor señal.

Bonus: métodos endless

La 3.0 también trajo los endless methods, una sintaxis para métodos de una expresión:

def total(items) = items.sum(&:price)

Generó su minidebate ("¡eso no es Ruby!") y luego se normalizó. Para predicados y delegaciones de una línea, hoy me parece lo natural. También llegaron pequeñeces agradables como Hash#except —que ActiveSupport llevaba años regalándonos— o Symbol#name.

Qué quedó de todo aquello

Mirando la 3.0 desde 2026, el reparto es curioso. Lo que abría los titulares —Ractor, el 3x3— se quedó en promesa o en benchmark. Lo que parecía secundario resultó ser lo importante: el pattern matching se convirtió en Ruby idiomático, el Fiber Scheduler sembró el ecosistema async, y la separación de kwargs saneó el lenguaje aunque nos costara sudor. Ruby 3.0 no fue la versión rápida; fue la versión que preparó el terreno para que las siguientes lo fueran.

En la próxima entrega de la serie: Ruby 3.1 y el momento en que Shopify puso un JIT encima de la mesa que sí entendía Rails. Todas las entregas, bajo la etiqueta ruby.