Sorbet y RBS en Ruby: tipado estático en el lenguaje más dinámico

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP