Ruby es dinámico por diseño. Las variables no tienen tipo declarado, los métodos se pueden redefinir en tiempo de ejecución y el duck typing es una de sus señas de identidad. Esto hace que sea muy productivo para escribir código rápido, pero también que los errores de tipo solo aparezcan cuando el código se ejecuta. Para bases de código grandes, eso es un problema.
Dos soluciones han cobrado relevancia: Sorbet, el type checker de Stripe, y RBS, el formato oficial de firmas de tipo que Ruby incorporó en la versión 3.0.
Sorbet: tipado gradual con sintaxis Ruby
Sorbet es un verificador de tipos desarrollado por Stripe para su propia base de código, que tiene millones de líneas de Ruby. Lo publicaron como open source en 2019. La premisa es que puedes adoptarlo de forma gradual: empiezas sin anotar nada, y vas añadiendo tipos a los archivos que más te interesen.
Las anotaciones de tipo en Sorbet usan métodos de Ruby que se definen en tiempo de ejecución, así que el código es válido Ruby incluso sin Sorbet instalado:
# typed: true
require 'sorbet-runtime'
class Usuario
extend T::Sig
sig { returns(String) }
def nombre
@nombre
end
sig { params(nombre: String).void }
def nombre=(nombre)
@nombre = nombre
end
sig { params(edad: Integer).returns(T::Boolean) }
def mayor_de_edad?(edad)
edad >= 18
end
end
# Tipado de argumentos opcionales y nilables
sig { params(email: T.nilable(String)).returns(String) }
def normalizar_email(email)
email&.downcase&.strip || ''
end
El método sig define la firma del método siguiente. T::Sig es el módulo que provee esta funcionalidad. En tiempo de desarrollo, el checker estático de Sorbet analiza estos bloques para detectar inconsistencias. En producción, el runtime también puede verificar los tipos si lo configuras.
Los niveles de strictness de Sorbet
Uno de los aciertos de diseño de Sorbet es que tiene niveles de verificación por archivo. Esto facilita la adopción gradual:
# typed: false Solo comprueba errores de sintaxis obvios # typed: true Verifica los sigs que hayas escrito # typed: strict Todos los métodos deben tener sig # typed: strong Máxima verificación, T.untyped prohibido
Puedes empezar poniendo # typed: false en todos los archivos (Sorbet los ignora) y luego ir subiendo el nivel en los módulos que más te interese cubrir. Stripe tardó varios años en llevar toda su base de código a typed: true.
RBS: el estándar oficial de Ruby
RBS (Ruby Signature) es el formato oficial para describir los tipos de Ruby, incluido desde Ruby 3.0 (diciembre de 2020). A diferencia de Sorbet, las firmas van en ficheros separados con extensión .rbs, no mezcladas con el código.
# lib/usuario.rbs class Usuario attr_reader nombre: String def initialize: (nombre: String) -> void def mayor_de_edad?: (Integer) -> bool def normalizar_email: (String?) -> String end
La sintaxis es diferente a Ruby normal. Los tipos se escriben directamente: String, Integer, String? para nilable, Array[String] para arrays tipados. Las definiciones de método usan -> para indicar el tipo de retorno.
Ruby incluye definiciones RBS para toda su librería estándar. Las gemas populares las van añadiendo también, publicadas en el repositorio gem_rbs_collection.
Steep y TypeProf: tools que usan RBS
RBS es solo el formato. Para verificar los tipos necesitas una herramienta encima. Las dos principales son:
- Steep: type checker que usa ficheros RBS para analizar el código Ruby. Similar conceptualmente a Sorbet pero con el formato estándar.
- TypeProf: incluido en Ruby desde 3.0, analiza el código Ruby y genera ficheros RBS automáticamente. Útil como punto de partida para documentar tipos de código existente.
# Instalar y configurar Steep # Gemfile gem 'steep', require: false # Steepfile (configuración) target :lib do signature 'sig' # Directorio con ficheros .rbs check 'lib' # Directorio a verificar library 'pathname' end # Ejecutar verificación $ steep check
Sorbet vs RBS: cuál elegir
Depende del proyecto. Sorbet es más maduro para equipos grandes con bases de código existentes: tiene mejor integración con editores (el plugin para VS Code y RubyMine es muy bueno), mejor inferencia de tipos y un ecosistema de tipos para gemas populares más completo.
RBS es el estándar oficial del lenguaje. Si estás empezando un proyecto nuevo o quieres algo que funcione a largo plazo sin dependencias adicionales de Stripe, RBS con Steep es la opción más alineada con el futuro de Ruby.
Muchos equipos usan ambos: Sorbet para el código de aplicación (porque es más productivo de escribir) y RBS para las gemas públicas que quieren documentar de forma estándar.
El tipado en la práctica
El mayor obstáculo para adoptar tipado en Ruby no es técnico sino cultural. Ruby se escribe rápido precisamente porque no hay que declarar tipos. Añadir sig o .rbs es trabajo extra que el equipo tiene que valorar.
La experiencia de Stripe sugiere que el punto de inflexión llega cuando el proyecto supera unas 50.000-100.000 líneas y el equipo crece. A partir de ahí, los errores de tipo en producción empiezan a doler más que el coste de mantener las firmas.
Para proyectos más pequeños o prototipado, el tipado dinámico de Ruby sigue siendo una ventaja. No hace falta adoptar Sorbet o RBS en todos los proyectos: una buena cobertura de tests con RSpec consigue objetivos similares con menos fricción. En el artículo sobre TDD con PHP y Laravel hay un buen contraste de cómo el testing cubre parte de lo que el tipado estático ofrece en otros lenguajes.
Imagen: Pexels / Markus Spiske
