Skip to content

Ruby Hoja de referencia

Lenguaje dinámico y elegante optimizado para la felicidad del desarrollador.

01

Fundamentos

Variables y Tipos

Ruby es de tipado dinámico—las variables no necesitan declaraciones de tipo. Use .class para inspeccionar el tipo e is_a? para verificaciones de tipo. Todo es un objeto, incluyendo números y booleanos.

ruby
name = "Alice"    # string
age = 30           # integer
pi = 3.14          # float
is_dev = true      # boolean
nums = [1, 2, 3]   # array
puts name.class    # String
puts age.is_a?(Integer)  # true

Símbolos

Los símbolos (:name) son cadenas inmutables e internadas—solo existe una copia en memoria. Úselos para claves de hash, nombres de métodos e identificadores donde la identidad importa más que el contenido. Más eficientes en memoria que las cadenas para uso repetido.

ruby
status = :active
puts status.class        # Symbol
puts status.to_s         # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status]       # active

Nil y Truthiness

En Ruby, solo nil y false son falsy—todo lo demás (incluyendo 0, '' y []) es truthy. Esto difiere de muchos lenguajes donde 0 es falsy. Use || para valores predeterminados y nil? para verificar nil específicamente.

ruby
x = nil
puts x.nil?        # true
puts x || "default"  # default
# Only nil and false are falsy; 0 and "" are truthy!
puts 0 ? "truthy" : "falsy"  # truthy
puts "" ? "truthy" : "falsy" # truthy
puts nil ? "truthy" : "falsy" # falsy

Conversión de Tipos

to_i/to_s/to_f son conversiones indulgentes (devuelven 0 en fallo). Integer()/Float() son estrictas (lanzan ArgumentError). Use conversión estricta cuando necesite validación, indulgente cuando quiera degradación elegante.

ruby
puts "42".to_i      # 42
puts 42.to_s        # "42"
puts "3.14".to_f    # 3.14
puts 3.14.to_i      # 3
puts "abc".to_i     # 0 (no error)
puts Integer("42")  # 42 (raises if invalid)

Interpolación de Cadenas

Las cadenas con comillas dobles admiten interpolación #{expr}—cualquier expresión de Ruby dentro. Las cadenas con comillas simples son literales (sin interpolación ni escapes excepto \\ y \'). Use comillas dobles cuando necesite interpolación o secuencias de escape.

ruby
name = "Alice"
age = 30
puts "Name: #{name}, Age: #{age}"
puts "5 + 3 = #{5 + 3}"
puts "Upper: #{name.upcase}"
# Single quotes don't interpolate
puts 'No #{name} here'  # No #{name} here
02

Cadenas

Métodos Comunes de Cadena

Las cadenas de Ruby tienen métodos ricos. La mayoría devuelven nuevas cadenas (las cadenas son mutables en Ruby). Use variantes ! (upcase!, gsub!) para modificación in situ—estas modifican el receptor y devuelven nil si no hay cambio.

ruby
s = "Hello, World"
puts s.length         # 12
puts s.upcase         # HELLO, WORLD
puts s.downcase       # hello, world
puts s.reverse        # dlroW ,olleH
puts s.split(", ")    # ["Hello", "World"]
puts s.gsub("o", "0") # Hell0, W0rld

Mutación de Cadenas (Métodos Bang)

Los métodos que terminan con ! modifican el objeto in situ y a menudo son más eficientes. Pueden devolver nil si no se hizo cambio. Use cuando quiera evitar crear copias, pero sea consciente de los efectos secundarios.

ruby
s = "hello"
s.upcase!    # s is now "HELLO"
s.gsub!(/L/, "1")  # s is now "HE11O"
puts s       # HE11O
# Bang methods modify in-place; use with caution
arr = [3, 1, 2]
arr.sort!    # arr is now [1, 2, 3]

Heredoc y Multilínea

Los heredocs (<<TEXT ... TEXT) crean cadenas multilínea. <<~ (squiggly heredoc) elimina el espacio en blanco inicial común para código limpio. Útiles para consultas SQL, plantillas HTML o mensajes largos.

ruby
text = <<~HEREDOC
  Hello,
  World!
  Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentation

Formato (sprintf)

Use el operador % o format/sprintf para formato de cadenas estilo C. %-10s alinea a la izquierda en 10 caracteres, %08.2f rellena con ceros a 8 caracteres con 2 decimales. Útil para salida tabular y formato de ancho fijo.

ruby
puts sprintf("%s is %d", "Alice", 30)
puts "%-10s|%5d" % ["Name", 42]
puts format("%.2f", 3.14159)  # 3.14
puts "%08.2f" % 3.14          # 00003.14
# %s string, %d integer, %f float

Concatenación de Cadenas

+ crea una nueva cadena, << añade in situ (más eficiente para construir cadenas). * repite una cadena. join combina elementos de array con un separador. Prefiera << o join sobre + repetido por rendimiento.

ruby
s1 = "Hello" + ", " + "World"
s2 = ["a", "b", "c"].join("-")  # a-b-c
s3 = "Hello"
s3 << " " << "World"  # in-place append
s4 = "x" * 3          # xxx (repetition)
puts s1, s2, s3, s4
03

Estructuras de Datos

Arrays

Los arrays son ordenados, con indexación base cero y pueden contener tipos mixtos. << y push añaden al final. Use include? para pertenencia, sum para totales. Los arrays son mutables—use freeze para hacerlos inmutables.

ruby
nums = [1, 2, 3, 4, 5]
nums.push(6)          # [1,2,3,4,5,6]
nums << 7             # same as push
nums[0] = 0
puts nums.first       # 0
puts nums.length      # 7
puts nums.sum         # 28
puts nums.include?(3) # true

Hashes

Los hashes son diccionarios clave-valor. Las claves de símbolo (name:) son idiomáticas y eficientes. Use key? para existencia, fetch para acceso seguro (lanza en ausencia), transform_values para actualizaciones masivas.

ruby
user = { name: "Alice", age: 30 }
user[:email] = "[email protected]"
puts user[:name]          # Alice
puts user.key?(:name)     # true
user.each { |k, v| puts "#{k}: #{v}" }
puts user.values          # ["Alice", 30, "[email protected]"]
puts user.transform_values(&:to_s)

Ranges

'..' incluye el final, '...' lo excluye. Los rangos son perezosos y eficientes en memoria para secuencias grandes. Útiles para iteración, slicing y generación de secuencias. Pueden usarse con cualquier tipo Comparable.

ruby
(1..5).each { |n| puts n }      # 1 2 3 4 5
puts (1...5).to_a                # [1, 2, 3, 4]
puts (1..10).select(&:even?)     # [2, 4, 6, 8, 10]
puts ('a'..'e').include?('c')    # true
puts (1..5).map { |n| n ** 2 }   # [1, 4, 9, 16, 25]

Métodos Enumerable

Enumerable es el mixin más potente de Ruby—map, select, reject, reduce, find, group_by, sort_by, etc. Use &:method como abreviatura de { |x| x.method }. Estos habilitan pipelines expresivos de transformación de datos.

ruby
nums = [1, 2, 3, 4, 5]
puts nums.map { |n| n * 2 }.inspect    # [2,4,6,8,10]
puts nums.select(&:even?).inspect      # [2, 4]
puts nums.reject(&:odd?).inspect       # [2, 4]
puts nums.reduce(0) { |sum, n| sum + n } # 15
puts nums.find { |n| n > 3 }           # 4
puts nums.group_by(&:even?).inspect    # {false=>[1,3,5], true=>[2,4]}

Sets

Set (de la biblioteca 'set') almacena elementos únicos con búsqueda O(1). Use para desduplicación y operaciones de conjuntos (unión, intersección, diferencia). Convierta con to_a cuando necesite un array. Requiere 'set' require.

ruby
require 'set'
a = Set.new([1, 2, 3])
b = Set.new([3, 4, 5])
puts a.union(b).to_a.inspect        # [1,2,3,4,5]
puts a.intersection(b).to_a.inspect # [3]
puts a.subtract([1]).to_a.inspect   # [2, 3]
puts a.subset?(Set.new([1,2,3,4]))  # true
a.add(6)
04

Flujo de Control

If / Elsif / Unless

if/elsif/else es ramificación estándar. unless es lo opuesto de if (se ejecuta cuando la condición es falsa). La forma modificadora (instrucción if condición) es idiomática para guardias de una línea—mejora la legibilidad para casos simples.

ruby
score = 85
if score >= 90
  puts "A"
elsif score >= 80
  puts "B"
else
  puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60

Case (When)

case/when usa === para coincidencia, habilitando rangos, clases y regex. Múltiples valores separados por comas. then permite cuerpos de una línea. Sin un objetivo, case actúa como una cadena if/elsif más limpia.

ruby
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
end

While / Until / Loop

while se ejecuta mientras true, until se ejecuta hasta que true (mientras false). loop es infinito—use break para salir. Prefiera iteradores (each, map) sobre while para colecciones—son más idiomáticos y menos propensos a errores.

ruby
count = 0
while count < 3
  puts count
  count += 1
end
# until = opposite of while
n = 3
until n == 0
  puts n
  n -= 1
end
# Infinite loop with break
loop do
  puts "forever"
  break if rand > 0.8
end

Iteradores (Each/Times/Upto)

Los iteradores de Ruby son más idiomáticos que los bucles for. times para contar, upto/downto para rangos, each_with_index para índice+valor. step controla el incremento. Estos son la columna vertebral de la iteración en Ruby.

ruby
3.times { |i| puts i }          # 0 1 2
1.upto(3) { |n| puts n }        # 1 2 3
3.downto(1) { |n| puts n }      # 3 2 1
[1,2,3].each_with_index do |n, i|
  puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n }   # 1 2 3

Break / Next / Redo

next salta a la siguiente iteración (como continue), break sale del bucle temprano. break puede devolver un valor de un bloque. redo reinicia la iteración actual sin volver a verificar la condición—raramente usado.

ruby
[1, 2, 3, 4, 5].each do |n|
  next if n.even?    # skip even
  break if n > 4     # stop at 5
  puts n             # prints 1, 3
end
# break value returns from the block
result = [1,2,3].each { |n| break n * 10 if n == 2 }
puts result  # 20
05

Métodos y Bloques

Definición de Métodos

Los métodos usan def/end. Los argumentos de palabra clave (key:) mejoran la legibilidad para muchos parámetros. *args recolecta argumentos posicionales extra en un array, **kwargs recolecta argumentos de palabra clave en un hash. Los valores predeterminados usan =.

ruby
def greet(name, greeting: "Hello")
  "#{greeting}, #{name}!"
end
puts greet("Alice")               # Hello, Alice!
puts greet("Bob", greeting: "Hi") # Hi, Bob!
def add(*nums)  # splat (variadic)
  nums.sum
end
puts add(1, 2, 3, 4)  # 10

Bloques y Yield

Los bloques son fragmentos de código anónimos pasados a métodos. yield invoca el bloque. Los bloques están en todas partes en Ruby (each, map, etc.). Use yield para hacer sus métodos flexibles—los llamadores proporcionan el comportamiento.

ruby
def repeat(n)
  n.times { yield }
end
repeat(3) { puts "hi" }  # prints hi 3 times
def with_result
  yield(5)
end
puts with_result { |x| x * 2 }  # 10

Procs y Lambdas

Los Procs y lambdas son bloques reutilizables almacenados en variables. Diferencia clave: las lambdas verifican el conteo de argumentos (lanzan en desajuste) y solo retornan desde sí mismas; los procs son indulgentes y retornan desde el método envolvente. Prefiera lambda para comportamiento estricto.

ruby
square = proc { |x| x * x }
puts square.call(5)   # 25
puts square.(5)       # 25 (shortcut)
double = lambda { |x| x * 2 }
puts double.call(5)   # 10
# Lambda checks arity; Proc doesn't
# Lambda returns from itself; Proc returns from enclosing method

Objeto Method (&)

&:method convierte un nombre de método a un Proc. Es la abreviatura idiomática para bloques de un solo método: map(&:to_i) en lugar de map { |s| s.to_i }. Más limpio y legible para transformaciones simples.

ruby
nums = ["1", "2", "3"]
ints = nums.map(&:to_i)  # [1, 2, 3]
puts ints.inspect
# &:to_i is shorthand for { |s| s.to_i }
names = ["alice", "bob"]
up = names.map(&:upcase)  # ["ALICE", "BOB"]
puts up.inspect

Valores de Retorno

Los métodos de Ruby devuelven implícitamente la última expresión evaluada—sin necesidad de return explícito. Use return para salidas tempranas. Múltiples valores se devuelven como un array y pueden deconstruirse. Esto hace el código conciso.

ruby
def status(ok)
  return "error" unless ok
  "ok"  # implicit return of last expression
end
puts status(true)   # ok
puts status(false)  # error
# Multiple assignment
def coords
  return 1, 2  # returns [1, 2]
end
x, y = coords
06

Clases y POO

Clase y Variables de Instancia

@var = variable de instancia (por objeto), @@var = variable de clase (compartida). attr_accessor genera getter+setter, attr_reader solo getter, attr_writer solo setter. self.method define métodos de clase.

ruby
class Person
  attr_accessor :name, :age
  attr_reader :id
  @@count = 0  # class variable
  def initialize(name, age)
    @name = name  # instance variable
    @age = age
    @@count += 1
  end
  def self.count; @@count; end
end
p = Person.new("Alice", 30)
puts p.name, Person.count

Herencia y Super

< denota herencia (solo herencia simple). super llama la versión del padre del método actual. Use super (con paréntesis para pasar argumentos, sin paréntesis para pasar los mismos argumentos) para extender el comportamiento del padre. Ruby usa herencia simple + mixins.

ruby
class Animal
  def initialize(name); @name = name; end
  def speak; "..."; end
end
class Dog < Animal
  def speak; "#{@name}: Woof!"; end
end
class Puppy < Dog
  def speak; "#{super} (small)"; end
end
puts Puppy.new("Rex").speak  # Rex: Woof! (small)

Módulos (Mixins)

Los módulos agrupan métodos reutilizables. include añade métodos de instancia, extend añade métodos de clase. Esta es la solución de Ruby a la herencia múltiple. Enumerable es un mixin famoso—inclúyalo y defina each para obtener map, select, etc.

ruby
module Walkable
  def walk; "#{@name} is walking"; end
end
module Swimmable
  def swim; "#{@name} is swimming"; end
end
class Duck
  include Walkable    # instance methods
  extend Swimmable    # class methods
  def initialize(n); @name = n; end
end
puts Duck.new("Donald").walk  # Donald is walking

Control de Acceso

public (predeterminado), private (solo llamable sin receptor explícito), protected (llamable dentro de la jerarquía de clases). Use private para helpers internos, protected para métodos compartidos entre instancias de la misma clase.

ruby
class BankAccount
  def initialize(bal); @balance = bal; end
  def deposit(amt); @balance += amt; end
  def balance; @balance; end
  private
  def audit; "auditing..."; end
  protected
  def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance  # 100
# a.audit  # Error: private method

Self y Métodos de Clase

self se refiere al objeto actual. Dentro del cuerpo de la clase, self es la clase—def self.method define métodos de clase. Los métodos de clase se llaman en la clase (Counter.total), los métodos de instancia en instancias. alias_method crea un alias de método.

ruby
class Counter
  @@total = 0
  def initialize; @@total += 1; end
  def self.total; @@total; end
  def self.reset!; @@total = 0; end
  def instance_method; "I'm an instance"; end
  alias_method :count, :instance_method
end
Counter.new; Counter.new
puts Counter.total  # 2
07

Manejo de Errores

Begin / Rescue / Ensure

begin/rescue es el try/catch de Ruby. Rescate clases de excepción específicas para manejo dirigido. => e captura el objeto de excepción. ensure se ejecuta siempre—úselo para limpieza (cerrar archivos, liberar locks).

ruby
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "Caught: #{e.message}"
rescue => e
  puts "Other error: #{e.class}"
ensure
  puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failure

Raise y Excepciones Personalizadas

raise lanza una excepción (raise sin argumentos relanza). Las excepciones personalizadas heredan de StandardError (o una clase más específica). Nómbrelas con sufijo Error. Rescate por clase para manejar modos de fallo específicos.

ruby
class InvalidAgeError < StandardError; end
def set_age(age)
  raise InvalidAgeError, "Age cannot be negative" if age < 0
  raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
  @age = age
end
begin
  set_age(-5)
rescue InvalidAgeError => e
  puts "Custom: #{e.message}"
end

Retry

retry reinicia el bloque begin desde el principio. Use para fallos transitorios (red, límites de tasa) con un contador para evitar bucles infinitos. Sin un límite, retry puede colgar su programa—siempre protéjalo.

ruby
attempts = 0
begin
  attempts += 1
  fetch_data  # might fail
rescue NetworkError
  retry if attempts < 3
  puts "Failed after 3 attempts"
end
# retry re-runs the begin block from the top

Modificador Rescue

rescue como modificador es una forma concisa de proporcionar un valor alternativo. Captura StandardError y devuelve el lado derecho. Use para casos simples—evítelo para lógica compleja ya que oculta errores. Genial para análisis u operaciones opcionales.

ruby
result = risky_operation rescue "default"
puts result
# Same as:
# result = begin; risky_operation; rescue; "default"; end
# Catches StandardError only
json = JSON.parse(str) rescue nil
puts "Invalid JSON" if json.nil?

Throw / Catch (Flujo de Control)

throw/catch NO es manejo de excepciones—es un mecanismo de flujo de control para salida temprana de anidamiento profundo (a diferencia de otros lenguajes). throw :symbol salta al catch coincidente. Use para salir de bucles anidados; use begin/rescue para errores reales.

ruby
catch(:done) do
  [1, 2, 3, 4, 5].each do |n|
    throw :done, n if n > 3
    puts n
  end
end
# Prints 1, 2, 3 and returns 4
# Not for errors—use for early exit from nested loops
08

E/S de Archivos

Leer y Escribir Archivos

File.write/File.read son métodos simples de una sola vez. File.open con un bloque auto-cierra el archivo. File.foreach lee línea por línea sin cargar todo el archivo—eficiente en memoria para archivos grandes. chomp elimina el salto de línea final.

ruby
# Write
File.write("test.txt", "Hello, File!")
# Read
content = File.read("test.txt")
puts content  # Hello, File!
# Append
File.open("log.txt", "a") { |f| f.puts "new line" }
# Read line by line
File.foreach("test.txt") { |line| puts line.chomp }

Bloque de Archivo (Auto-Cierre)

Siempre use File.open con un bloque—garantiza que el archivo se cierre incluso si ocurre un error. La forma de bloque es la forma idiomática y segura de manejar archivos. Sin un bloque, debe llamar close manualmente.

ruby
File.open("data.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
end  # file auto-closed here
File.open("data.txt", "r") do |f|
  f.each_line.with_index { |line, i| puts "#{i}: #{line}" }
end

Existencia e Información de Archivo

La clase File proporciona consultas de sistema de archivos. file?/directory? distinguen tipos. mtime/ctime/atime dan marcas de tiempo. rename/delete modifican el sistema de archivos. Siempre verifique la existencia antes de operaciones para evitar errores.

ruby
puts File.exist?("test.txt")  # true
puts File.file?("test.txt")   # true (regular file)
puts File.directory?(".")     # true
puts File.size("test.txt")    # bytes
puts File.mtime("test.txt")   # modification time
File.rename("old.txt", "new.txt")
File.delete("new.txt") if File.exist?("new.txt")

Operaciones de Directorio

Dir gestiona directorios. mkdir crea, chdir cambia (la forma de bloque restaura después). glob coincide patrones de archivo—* coincide cualquier, ** coincide recursivamente. Útil para descubrimiento de archivos y procesamiento por lotes.

ruby
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
  File.write("a.txt", "a")
  puts Dir.pwd  # current path
end
Dir.glob("*.txt") { |f| puts f }  # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursive

CSV y JSON

CSV y JSON están en la biblioteca estándar. CSV.foreach transmite filas (eficiente en memoria). JSON.parse devuelve hashes/arrays con claves de cadena. to_json serializa cualquier objeto. Estos son esenciales para intercambio de datos.

ruby
require 'csv'
require 'json'
CSV.write("data.csv", [["a", 1], ["b", 2]])
CSV.foreach("data.csv") { |row| puts row.inspect }
data = { name: "Alice", age: 30 }
File.write("data.json", data.to_json)
parsed = JSON.parse(File.read("data.json"))
puts parsed["name"]  # Alice
09

Fecha/Hora y Regex

Time y Date

Time representa un momento (con zona horaria). Date representa una fecha de calendario (sin hora). La aritmética en Time usa segundos. strftime formatea con %Y (año), %m (mes), %d (día), %H:%M (hora). Require 'time' para análisis ISO8601.

ruby
require 'time'
now = Time.now
puts now                    # 2024-01-15 14:30:00 +0800
puts now.strftime("%Y-%m-%d %H:%M")  # 2024-01-15 14:30
tomorrow = now + 86400      # +1 day (seconds)
puts tomorrow.strftime("%A") # weekday name
date = Date.today
puts date.next_day          # tomorrow

Análisis y Aritmética de Fechas

La aritmética de fechas funciona naturalmente—añadir enteros añade días. next_month/prev_month manejan límites de mes. Date - Date devuelve un Rational (días). upto/downto iteran sobre rangos de fechas. Use Date para lógica de calendario, Time para marcas de tiempo.

ruby
require 'date'
d = Date.parse("2024-01-15")
puts d.year   # 2024
puts d + 7    # 2024-01-22 (+7 days)
puts d.next_month  # 2024-02-15
diff = (Date.today - d).to_i
puts "#{diff} days since"
puts Date.today.upto(Date.today + 6).map(&:wday)

Coincidencia Regex

=~ devuelve la posición de coincidencia o nil. $1, $2 contienen grupos capturados después de una coincidencia. .match devuelve un objeto MatchData para más detalle. Use =~ para verificaciones simples, .match para extraer capturas. Los literales regex usan /pattern/.

ruby
s = "Phone: 123-4567"
if s =~ /(\d+)-(\d+)/
  puts "Area: #{$1}, Number: #{$2}"
end
m = /\w+@(\w+)/.match("[email protected]")
puts m[1]  # example
puts "abc123" =~ /\d/ ? "has digit" : "no digit"

Sustitución Regex

gsub reemplaza todas las coincidencias (sub reemplaza la primera). Pase un bloque para reemplazo dinámico. scan extrae todas las coincidencias en un array. Las referencias inversas (\1, \2) en cadenas de reemplazo se refieren a grupos capturados. Potente para procesamiento de texto.

ruby
s = "Hello, World"
puts s.gsub(/o/, "0")       # Hell0, W0rld
puts s.gsub(/\w+/) { |w| w.capitalize }
puts s.scan(/\w+/).inspect  # ["Hello", "World"]
puts "2024-01-15".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
# => 15/01/2024

Opciones Regex

Opciones regex: i (sin distinción de mayúsculas), m (multilínea—el punto coincide nueva línea), x (extendido—permite espacios/comentarios en el patrón). %r{} es un delimitador alternativo útil cuando el patrón contiene barras (como URLs).

ruby
puts /hello/i =~ "HELLO"  # 0 (case-insensitive)
puts /line/m =~ "a\nb"    # 0 (multiline: . matches \n)
puts /x.y/x =~ "x y"       # 0 (extended: ignore whitespace)
# Common patterns
email = /[\w.]+@[\w]+\.[a-z]+/
url = %r{https?://[\w./]+}
puts email.match("[email protected]") ? "valid" : "invalid"
10

Metaprogramación y Concurrencia

Definición Dinámica de Métodos

define_method crea métodos en tiempo de ejecución. Use para generar múltiples métodos similares o construir DSLs. Esto es metaprogramación—código que escribe código. Potente pero úselo con juicio; puede hacer el código más difícil de entender.

ruby
class Dynamic
  [:foo, :bar, :baz].each do |name|
    define_method(name) { puts "Called #{name}" }
  end
end
d = Dynamic.new
d.foo  # Called foo
d.bar  # Called bar
# Useful for generating similar methods

method_missing

method_missing intercepta llamadas a métodos no definidos. Use para construir APIs flexibles o proxies. Siempre sobrescriba respond_to_missing? también, para que la reflexión funcione. Use con moderación—puede ocultar bugs y confundir al análisis estático.

ruby
class Proxy
  def method_missing(name, *args)
    puts "Called: #{name} with #{args.inspect}"
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end
p = Proxy.new
p.anything(1, 2)  # Called: anything with [1, 2]

Threads

Los threads ejecutan código concurrentemente. join espera la finalización. MRI (Ruby estándar) tiene un GIL, así que los threads limitados por CPU no se ejecutan en verdadero paralelo—los limitados por E/S sí. Para paralelismo real, use Ractor (Ruby 3.0+) o múltiples procesos.

ruby
threads = [1, 2, 3].map do |n|
  Thread.new { puts "Thread #{n}: #{n * n}" }
end
threads.each(&:join)  # wait for all
# MRI has GIL—threads don't run truly parallel for CPU work
# Use for I/O concurrency (network, file)

Fibers (Cooperativas)

Las fibers son concurrencia cooperativa ligera—se pausan y reanudan manualmente vía yield/resume. A diferencia de los threads, no se ejecutan en paralelo. Use para generadores, evaluación perezosa o computaciones pausables. Menor sobrecarga que los threads.

ruby
fiber = Fiber.new do
  Fiber.yield 1
  Fiber.yield 2
  3
end
puts fiber.resume  # 1
puts fiber.resume  # 2
puts fiber.resume  # 3
# Fibers yield control cooperatively, not preemptively

Send y Eval

send llama cualquier método por nombre (incluyendo privados). public_send respeta la visibilidad. eval ejecuta una cadena como código Ruby—extremadamente potente pero peligroso con entrada no confiable (inyección de código). Use send para despacho dinámico, evite eval en producción.

ruby
class Obj
  def secret; "hidden"; end
end
o = Obj.new
puts o.send(:secret)  # hidden (calls private too)
puts o.public_send(:secret)  # hidden (respects visibility)
x = 5
result = eval("x * 2")
puts result  # 10
# eval runs a string as code—avoid with untrusted input!
11

Bloques, Procs y Lambdas

Fundamentos de Bloques

Los bloques son el closure más común de Ruby—código anónimo pasado a métodos vía { } o do...end. yield invoca el bloque. |n| declara parámetros de bloque. block_given? verifica si se pasó un bloque. Los bloques no son objetos (no pueden almacenarse en variables)—use Proc/Lambda para eso. Cada método puede aceptar un bloque implícito, haciendo los DSLs naturales (Rails usa esto extensivamente).

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }          # do-end for multi-line
[1, 2, 3].each do |n|
  puts n * 2
end

# yield: call the block from a method
def greet
  puts "before"
  yield  # invokes the block
  puts "after"
end
greet { puts "in block" }
# before / in block / after

# yield with arguments
def compute
  result = yield(10, 20)
  puts "got: #{result}"
end
compute { |a, b| a + b }  # got: 30

# block_given? checks if a block was passed
def maybe_yield
  return "no block" unless block_given?
  yield
end

Procs vs Lambdas

Los Procs y Lambdas son ambos objetos llamables (bloques convertidos en objetos). Los Procs son indulgentes: argumentos extra se vuelven nil, argumentos faltantes son nil, y 'return' sale del método envolvente. Las Lambdas son estrictas: verifican el conteo de argumentos y 'return' solo sale de la lambda. Use lambdas cuando quiera comportamiento tipo método; Procs cuando quiera comportamiento tipo bloque. -> (stabby lambda) es la sintaxis concisa moderna.

ruby
# Proc: a block stored as an object
p = Proc.new { |x| puts x * 2 }
p.call(5)      # 10
p.(5)          # 10 (shorthand)
p[5]           # 10 (another shorthand)

# Lambda: stricter Proc
l = lambda { |x| puts x * 2 }
l = ->(x) { puts x * 2 }  # stabby lambda syntax
l.call(5)

# Key differences:
# 1. Argument checking
p = Proc.new { |a, b| puts a }
p.call(1)        # 1 (b is nil, no error)
l = ->(a, b) { puts a }
l.call(1)        # ArgumentError (wrong number of args)

# 2. return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # never reached—Proc return exits method
end
proc_test  # 1

def lambda_test
  l = lambda { return 1 }
  l.call  # returns from lambda, not method
  return 2
end
lambda_test  # 2

Objetos Method

method(:name) recupera un método como un objeto Method (llamable como un Proc). &:symbol convierte un símbolo a un proc que envía ese método—idioma extremadamente común: array.map(&:to_s). El prefijo & convierte un Proc a un bloque (o viceversa). Esto habilita pasar métodos como argumentos elegantemente. Los objetos Method retienen su receptor, así que están vinculados al objeto.

ruby
class Calculator
  def add(a, b) a + b end
end

calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3)  # 5
m.(2, 3)      # 5
m[2, 3]       # 5

# Convert method to Proc
p = m.to_proc
p.call(4, 5)  # 9

# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase)  # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }

# Symbol#to_proc
:upcase.to_proc.call("hello")  # "HELLOW"

# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts))  # prints 1, 2, 3

Closures y Binding

Los closures (bloques, procs, lambdas) capturan variables por referencia—ven las actualizaciones a las variables capturadas. Esto habilita closures con estado (contadores, acumuladores). Proc#binding da acceso al entorno del closure (para metaprogramación avanzada). El método Kernel#binding captura el contexto de ejecución actual para eval. Los closures son por qué los bloques de Ruby son tan potentes para callbacks e iteradores.

ruby
# Closures capture surrounding variables
counter = 0
increment = lambda { counter += 1 }
increment.call  # 1
increment.call  # 2
puts counter    # 2 (closure modified it)

# Multiple closures share the same variables
x = 10
add = lambda { |n| x += n }
get = lambda { x }
add.call(5)
get.call  # 15

# Proc#binding: access the closure's environment
def make_counter
  count = 0
  lambda { count += 1 }
end
c = make_counter
c.call  # 1
# c.binding.eval("count")  # 1 (peek at captured var)

# Binding for eval in a specific context
b = binding
x = 42
eval("x", b)  # 42

Iteradores Personalizados con Bloques

Incluir Enumerable y definir #each le da map, select, reduce y 50+ métodos gratis—este es el protocolo iterador de Ruby. each_with_object es más limpio que inject para construir acumuladores. tap inserta efectos secundarios en cadenas de métodos (genial para depuración). Los métodos personalizados con yield le permiten crear sus propios DSLs (with_timing, with_database, etc.). Los bloques hacen que los patrones de iteración y callback de Ruby sean elegantes.

ruby
# Define your own iterator using yield
class LinkedList
  include Enumerable  # gets map, select, etc. for free

  def each
    node = @head
    while node
      yield node.value
      node = node.next
    end
  end
end

# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
  hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}

# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
         .tap { |arr| puts "after map: #{arr}" }
         .select { |n| n > 2 }

# Custom method with block
def with_timing
  start = Time.now
  yield
  puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }
12

Enumerable e Iteradores

Métodos Core de Enumerable

Enumerable es el mixin más potente de Ruby—inclúyalo y defina #each para obtener 50+ métodos. map transforma, select/reject filtran, reduce/inject agrega, find devuelve la primera coincidencia, group_by/partition agrupan. Las abreviaturas de símbolo (reduce(:+)) son idiomáticas. Estos métodos funcionan en Arrays, Hashes, Ranges, Files—cualquier cosa con #each. Dominar Enumerable es clave para Ruby idiomático.

ruby
nums = [1, 2, 3, 4, 5, 6]

# map/collect: transform
nums.map { |n| n * 2 }        # [2, 4, 6, 8, 10, 12]

# select/filter: keep matching
nums.select { |n| n.even? }   # [2, 4, 6]
nums.reject { |n| n.even? }   # [1, 3, 5]

# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n }  # 21
nums.reduce(:+)                        # 21 (symbol shorthand)
nums.reduce(:*)                        # 720

# find/detect: first match
nums.find { |n| n > 3 }       # 4

# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}

# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]

# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_a

Evaluación Perezosa

lazy convierte un enumerable en uno perezoso—los valores se calculan solo cuando se necesitan. Esto habilita secuencias infinitas y evita calcular toda la colección para cadenas que solo necesitan unos pocos resultados. Sin lazy, map/select crean arrays intermedios. Con lazy, el pipeline extrae valores uno a la vez. Use lazy para conjuntos de datos grandes/infinitos o transformaciones costosas. La contrapartida: lazy tiene sobrecarga por elemento, así que es más lento para colecciones pequeñas.

ruby
# Lazy: evaluate on demand (infinite sequences possible)
require 'prime'

# Without lazy: infinite loop!
# primes = (1..Float::INFINITY).select(&:prime?).first(5)

# With lazy: works!
primes = (1..Float::INFINITY).lazy.select(&:prime?).first(5)
# [2, 3, 5, 7, 11]

# Chain without intermediate arrays
(1..1_000_000).lazy
  .map { |n| n * 2 }
  .select { |n| n > 1_000_000 }
  .first(10)  # only computes what's needed

# Build a lazy enumerator
enum = Enumerator::Lazy.new(1..Float::INFINITY) do |yielder, n|
  yielder << n if n.prime?
end
enum.first(5)  # [2, 3, 5, 7, 11]

Enumerator e Iteración Externa

Enumerator envuelve una iteración como un objeto—puede llamar .next manualmente (iteración externa) en lugar de usar un bloque (iteración interna). Esto habilita pausar/reanudar iteración, hacer peek y crear secuencias infinitas. Enumerator.new con un bloque le permite construir iteradores personalizados (generadores). La mayoría de los métodos Enumerable devuelven Enumerators cuando se llaman sin un bloque: [1,2,3].map devuelve un Enumerator que puede encadenar.

ruby
# Enumerator: an iterator object you can control
enum = [10, 20, 30].each
enum.next  # 10
enum.next  # 20
enum.next  # 30
enum.next  # StopIteration

# External iteration with loop (handles StopIteration)
enum = [1, 2, 3].each
loop do
  puts enum.next
end

# Create custom Enumerator
fib = Enumerator.new do |yielder|
  a, b = 0, 1
  loop do
    yielder << a
    a, b = b, a + b
  end
end
fib.first(10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Convert to array
fib.take(10).to_a

Ordenamiento y Comparación

sort toma un bloque comparando dos elementos (devuelve -1, 0 o 1). sort_by es más eficiente—calcula la clave de ordenamiento una vez por elemento (transformación Schwartziana) en lugar de en cada comparación. Use sort_by para claves complejas. El operador <=> (spaceship) devuelve -1/0/1 y es la base del ordenamiento de Ruby. min/max/min_by/max_by encuentran extremos. Incluya Comparable y defina <=> para ordenamiento natural de sus objetos.

ruby
arr = [3, 1, 4, 1, 5, 9, 2, 6]

# sort: returns new array
arr.sort                    # [1, 1, 2, 3, 4, 5, 6, 9]
arr.sort { |a, b| b <=> a } # descending (9, 6, 5, ...)

# sort_by: more efficient (Schwartzian transform)
arr.sort_by { |n| -n }      # descending
words = ["banana", "apple", "cherry"]
words.sort_by { |w| w.length }  # ["apple", "banana", "cherry"]

# min/max
arr.min        # 1
arr.max        # 9
arr.minmax     # [1, 9]
arr.min_by { |n| n.abs }  # 1

# min/max with block
people.max_by { |p| p[:age] }

# <=> (spaceship): -1, 0, 1
1 <=> 2   # -1
2 <=> 2   # 0
3 <=> 2   # 1

Iteración y Transformación de Hash

Los Hashes son Enumerable también. each/each_pair iteran pares clave-valor. transform_keys/transform_values (Ruby 2.5+) crean nuevos hashes con claves/valores modificados. select/reject filtran en un nuevo hash. merge combina hashes (el bloque resuelve conflictos). group_by construye un hash desde un array. La iteración de Hash es la base del procesamiento de datos en Ruby—domine estos para manipulación de datos limpia y expresiva.

ruby
hash = { a: 1, b: 2, c: 3 }

# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }

# Transform keys/values
hash.transform_keys { |k| k.to_s }  # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}

# Filter
hash.select { |k, v| v > 1 }  # {b:2, c:3}
hash.reject { |k, v| v > 1 }  # {a:1}

# Merge
{ a: 1 }.merge({ b: 2 })  # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new }  # {a:3}

# Invert (values become keys)
{ a: 1, b: 2 }.invert  # {1=>:a, 2=>:b}

# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}
13

Gems y Bundler

Fundamentos de Gems

Las gems son paquetes de Ruby (bibliotecas). gem install gestiona gems del sistema. Para proyectos, use Bundler con un Gemfile para fijar versiones y gestionar dependencias. Especificadores de versión: '~> 1.4' (pesimista, permite parches), '>= 5.0' (optimista). Los grupos (:development, :test, :production) le permiten cargar solo las gems necesarias por entorno. require: false significa que Bundler no las auto-requerirá (las requiere manualmente cuando las necesite).

ruby
# Install a gem
$ gem install rails
$ gem install rails -v 7.0.0
$ gem install rails --pre  # pre-release

# List installed gems
$ gem list
$ gem list rails

# Uninstall
$ gem uninstall rails

# Gemfile: project dependencies
# Gemfile
source 'https://rubygems.org'
gem 'rails', '7.0.0'
gem 'pg', '~> 1.4'  # pessimistic: >= 1.4, < 2.0
gem 'puma', '>= 5.0'  # optimistic: >= 5.0
gem 'rspec', group: :test  # group-specific
gem 'pry', require: false  # don't auto-require

# Groups
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :production do
  gem 'newrelic_rpm'
end

Comandos de Bundler

Bundler asegura que su proyecto use las versiones exactas de gem especificadas. bundle install lee Gemfile y escribe Gemfile.lock (versiones exactas para reproducibilidad—¡commitee esto!). bundle exec ejecuta comandos con las versiones correctas de gem (evita conflictos). bundle update cambia versiones (tenga cuidado—puede romper cosas). Siempre use bundle exec para rake/rspec/rails para asegurar que las gems correctas se carguen. Gemfile.lock hace que los despliegues sean reproducibles.

ruby
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production  # skip group

# Update gems
$ bundle update              # update all
$ bundle update rails        # update specific gem
$ bundle outdated            # show outdated gems

# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate

# Check for dependency issues
$ bundle check
$ bundle doctor

# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!

# Add/remove gems
$ bundle add rspec
$ bundle remove rspec

# Clean old gems
$ bundle clean

Gemspec (Crear una Gem)

El archivo .gemspec define los metadatos y dependencias de una gem. spec.files lista los archivos incluidos; require_paths le dice a Ruby dónde encontrarlos. add_dependency para dependencias de tiempo de ejecución, add_development_dependency para dependencias de test/build. required_ruby_version exige la versión de Ruby. gem build crea el paquete .gem; gem install lo instala localmente. Publique a rubygems.org con gem push. Estructura: lib/ para código, spec/ para pruebas.

ruby
# my_gem.gemspec
Gem::Specification.new do |spec|
  spec.name = "my_gem"
  spec.version = "0.1.0"
  spec.summary = "A useful Ruby gem"
  spec.description = "Longer description..."
  spec.authors = ["Alice"]
  spec.email = ["[email protected]"]
  spec.homepage = "https://github.com/alice/my_gem"
  spec.license = "MIT"

  spec.files = Dir["lib/**/*.rb"] + ["README.md"]
  spec.require_paths = ["lib"]

  spec.required_ruby_version = ">= 3.0"

  spec.add_dependency "httparty", "~> 0.21"
  spec.add_development_dependency "rspec", "~> 3.12"
end

# Directory structure:
# my_gem/
#   lib/my_gem.rb        (main file)
#   lib/my_gem/version.rb
#   my_gem.gemspec
#   Gemfile
#   spec/                (tests)

# Build and install
$ gem build my_gem.gemspec  # creates my_gem-0.1.0.gem
$ gem install ./my_gem-0.1.0.gem

Tareas Rake

Rake es el make de Ruby—un ejecutor de tareas. Defina tareas con task :name do ... end. Los namespaces agrupan tareas relacionadas. Las tareas de archivo tienen dependencias (reconstruir si la fuente cambia). sh ejecuta comandos shell. Rake se usa para pruebas, builds, despliegues y tareas de base de datos (Rails lo usa extensivamente). Ejecute con rake task_name. La tarea default se ejecuta cuando escribe solo rake. Pase argumentos con rake greet[Alice].

ruby
# Rakefile
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec

# Custom task
task :greet, [:name] do |t, args|
  puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]

# Namespace
namespace :db do
  task :migrate do
    puts "migrating..."
  end
  task :seed do
    puts "seeding..."
  end
end
# $ rake db:migrate

# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
  sh "cp #{t.source} #{t.name}"
end

# Multi-line task
task :deploy do
  sh 'git push heroku main'
  sh 'heroku run rails db:migrate'
end

Rbenv y RVM (Versiones de Ruby)

rbenv y RVM gestionan múltiples versiones de Ruby en una máquina. rbenv es ligero (shims); RVM es más pesado (sobrescribe comandos shell). El archivo .ruby-version (commiteado) asegura que todos usen la misma versión de Ruby. Los gemsets (RVM) o bundle config path aíslan las gems del proyecto. Para producción, use bundle config set path para instalar gems localmente al proyecto, evitando contaminación de gems del sistema. Siempre fije la versión de Ruby en .ruby-version.

ruby
# rbenv: lightweight Ruby version manager
$ rbenv install 3.2.0        # install a version
$ rbenv global 3.2.0         # set global version
$ rbenv local 3.1.0          # set per-project (.ruby-version)
$ rbenv versions             # list installed

# .ruby-version file (committed to project)
# 3.2.0

# RVM: alternative version manager
$ rvm install 3.2.0
$ rvm use 3.2.0
$ rvm gemset create myapp    # isolated gem sets
$ rvm use 3.2.0@myapp

# Bundler config
$ bundle config set path 'vendor/bundle'  # install locally
$ bundle config set without 'development test'  # for production

# Check versions
$ ruby -v
$ gem -v
$ bundle -v
14

Fundamentos de Rails

Estructura MVC

Rails es un framework MVC: Models (ActiveRecord) manejan datos, Controllers (ActionController) manejan peticiones HTTP, Views (ActionView) renderizan respuestas. resources genera 7 rutas RESTful automáticamente. Las rutas mapean URLs a acciones de controlador. Convención sobre configuración: nombre su modelo User, controlador UsersController, y Rails conecta todo. Esta estructura es la columna vertebral de cada app Rails.

ruby
# Rails follows Model-View-Controller
# app/
#   models/    (ActiveRecord: data + business logic)
#   controllers/ (ActionController: handle requests)
#   views/     (ActionView: render responses)
#   helpers/   (view helpers)
# config/routes.rb (URL routing)

# Route → Controller → Model → View
# GET /users → UsersController#index → User.all → index.html.erb

# config/routes.rb
Rails.application.routes.draw do
  resources :users  # generates 7 RESTful routes
  # GET    /users          → index
  # GET    /users/new      → new
  # POST   /users          → create
  # GET    /users/:id      → show
  # GET    /users/:id/edit → edit
  # PATCH  /users/:id      → update
  # DELETE /users/:id      → destroy

  root 'pages#home'  # root path
  get 'about', to: 'pages#about'  # custom route
end

Modelos de ActiveRecord

ActiveRecord es el ORM de Rails—los modelos mapean a tablas de base de datos. validates impone integridad de datos. Las asociaciones (has_many, belongs_to, has_one) definen relaciones. Los callbacks (before_save, after_create) se enganchan al ciclo de vida. Los scopes son fragmentos de consulta reutilizables. ActiveRecord usa convención: modelo User → tabla users, columnas created_at/updated_at. Es el corazón de Rails—domínelo para desarrollo efectivo en Rails.

ruby
# app/models/user.rb
class User < ApplicationRecord
  # Validations
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, uniqueness: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }

  # Associations
  has_many :posts, dependent: :destroy
  has_one :profile
  belongs_to :company
  has_and_belongs_to_many :tags

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes (reusable queries)
  scope :active, -> { where(active: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  private
  def normalize_email
    self.email = email.downcase
  end
end

# Usage
User.create(name: "Alice", email: "[email protected]")
User.active.recent
User.find(1)
User.where("age > ?", 18)

Controladores y Strong Params

Los controladores manejan peticiones HTTP y coordinan modelos/vistas. before_action ejecuta filtros (autenticación, carga de recursos). Los parámetros strong (permit) previenen vulnerabilidades de asignación masiva—solo los campos de lista blanca pueden establecerse. redirect_to envía al usuario a otro lugar; render muestra una vista. Las variables @instance están disponibles en las vistas. Las acciones RESTful (index, show, new, create, edit, update, destroy) son convencionales.

ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: %i[show edit update destroy]

  def index
    @users = User.all
  end

  def show; end  # @user set by before_action

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User created'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  # Strong parameters: whitelist allowed fields
  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

Vistas y Helpers

ERB (Embedded Ruby) es el motor de plantillas por defecto de Rails: <%= %> genera salida, <% %> ejecuta. link_to/button_to generan enlaces/formularios HTML. form_with construye formularios vinculados a modelos. Los partials (_form.html.erb) son fragmentos de vista reutilizables renderizados con render. Los path helpers (new_user_path, user_path(user)) generan URLs desde rutas. Los helpers mantienen las vistas limpias. Para lógica compleja, use view helpers (app/helpers/) o decorators.

ruby
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= link_to "New User", new_user_path %>
<ul>
  <% @users.each do |user| %>
    <li>
      <%= link_to user.name, user_path(user) %>
      (<%= user.email %>)
      <%= link_to "Edit", edit_user_path(user) %>
      <%= button_to "Delete", user, method: :delete %>
    </li>
  <% end %>
</ul>

<!-- Partials: reusable view fragments -->
<!-- app/views/users/_form.html.erb -->
<%= form_with model: user do |form| %>
  <% if user.errors.any? %>
    <div class="errors">
      <% user.errors.full_messages.each do |msg| %>
        <p><%= msg %></p>
      <% end %>
    </div>
  <% end %>
  <%= form.text_field :name %>
  <%= form.email_field :email %>
  <%= form.submit %>
<% end %>

<!-- Render partial -->
<%= render "form", user: @user %>

Migraciones y Base de Datos

Las migraciones evolucionan el esquema de base de datos a lo largo del tiempo, con control de versiones. create_table define tablas; add_column/remove_column las modifican. t.references crea claves foráneas. t.timestamps añade created_at/updated_at. db:migrate aplica migraciones pendientes; db:rollback deshace la última. El archivo schema.rb es la fuente autoritativa del esquema actual (auto-generado). Nunca edite schema.rb directamente—use migraciones. Esto hace que los cambios de base de datos sean reproducibles entre entornos.

ruby
# Generate a migration
$ rails generate migration CreateUsers name:string email:string

# db/migrate/20240101_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false, index: { unique: true }
      t.integer :age, default: 0
      t.text :bio
      t.references :company, foreign_key: true

      t.timestamps  # created_at, updated_at
    end
  end
end

# Run migrations
$ rails db:migrate
$ rails db:rollback      # undo last migration
$ rails db:seed          # load seed data
$ rails db:reset         # drop + create + migrate + seed

# Schema file (don't edit manually)
# db/schema.rb reflects current database structure

# Add a column later
$ rails generate migration AddAgeToUsers age:integer
# creates: add_column :users, :age, :integer
15

Testing con RSpec

Sintaxis Básica (describe, it, expect)

RSpec es el framework de testing dominante de Ruby. describe agrupa pruebas relacionadas; it define una sola prueba. expect(...).to / not_to hacen aserciones. let define variables perezosas memoizadas (calculadas una vez al primer acceso). context es un alias de describe, usado para ramificaciones (when...). La gem shoulda-matchers proporciona sintaxis de una línea para validaciones/asociaciones comunes de Rails. Las pruebas van en spec/ reflejando la estructura de app/.

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: "Alice", email: "[email protected]") }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq("Alice")
    end

    # Multiple expectations with context
    context 'when name is blank' do
      let(:user) { User.new(name: "") }
      it 'is invalid' do
        expect(user).not_to be_valid
      end
    end
  end

  # One-liner syntax
  it { should validate_presence_of(:email) }
  it { should have_many(:posts) }
end

Mocks y Stubs

Los Stubs (allow) reemplazan valores de retorno de métodos; los mocks (expect) verifican que se llamó un método. Los Doubles son objetos falsos para testing (más rápidos que objetos reales). Use stubs para aislar el código bajo prueba de dependencias externas (APIs, bases de datos). Use mocks para verificar interacciones. El exceso de mocks hace las pruebas frágiles—prefiera objetos reales cuando sean suficientemente rápidos. FactoryBot crea datos de prueba; use create (guarda en DB) o build (solo en memoria).

ruby
RSpec.describe PaymentService do
  let(:user) { create(:user) }

  it 'charges the card' do
    # Stub: replace a method's return value
    allow(user).to receive(:premium?).and_return(true)

    # Mock: expect a method to be called
    expect(Stripe::Charge).to receive(:create).with(
      amount: 1000, currency: 'usd'
    ).and_return(double(id: 'ch_123'))

    service = PaymentService.new(user)
    result = service.charge(10)
    expect(result).to eq('ch_123')
  end

  it 'handles failure' do
    # Stub to raise an error
    allow(Stripe::Charge).to receive(:create)
      .and_raise(Stripe::StripeError.new("card declined"))

    service = PaymentService.new(user)
    expect { service.charge(10) }.to raise_error(PaymentError)
  end
end

# Double: test stand-in for an object
fake_card = double('Card', last4: '4242', brand: 'Visa')

FactoryBot y Fixtures

FactoryBot crea objetos de prueba con valores predeterminados sensatos. Los Traits crean variaciones (:admin, :inactive). Las sequences generan valores únicos (emails). create persiste a la base de datos; build no. create_list hace múltiples. Sobrescriba cualquier atributo pasándolo. Las factories son más flexibles que las fixtures (YAML) pero más lentas (escrituras en DB). Use build_stubbed para pruebas rápidas que no tocan la DB. Mantenga las factories simples—factories complejas indican modelos complejos.

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Alice" }
    email { "[email protected]" }
    age { 30 }
    active { true }

    # Traits: variations
    trait :admin do
      role { "admin" }
    end

    trait :inactive do
      active { false }
    end

    # Associations
    company { association(:company) }

    # Sequences for unique values
    sequence(:email) { |n| "user#{n}@example.com" }
  end
end

# Usage in specs
let(:user) { create(:user) }           # saved to DB
let(:admin) { create(:user, :admin) }  # with trait
let(:user) { build(:user) }            # not saved
let(:users) { create_list(:user, 5) }  # 5 users

# Override attributes
let(:alice) { create(:user, name: "Alice") }

Hooks Before/After

Los hooks before ejecutan código de setup: before(:each) (más común) antes de cada prueba, before(:all) una vez por grupo. Los hooks after limpian. DatabaseCleaner gestiona el estado de la base de datos de prueba (transacción para velocidad, truncación para exhaustividad). Prefiera let sobre before(:each)—let es perezoso (solo calcula cuando se usa) y memoizado, mientras before se ejecuta incluso si la prueba no lo necesita. Use before para efectos secundarios que deben ocurrir (logging, congelación de tiempo).

ruby
RSpec.describe User do
  before(:all) do
    # Runs once before all tests in this group
    @shared_data = load_expensive_data
  end

  before(:each) do  # or just 'before'
    # Runs before each test
    @user = User.create(name: "test")
  end

  after(:each) do
    # Runs after each test (cleanup)
    User.delete_all
  end

  after(:all) do
    # Runs once after all tests
    @shared_data = nil
  end

  # Database cleaner for transactional tests
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
end

# Use before for setup, after for cleanup
# Prefer let over before(:each) for lazy evaluation

Pruebas de Integración y Sistema

Las request specs prueban el stack completo (routing → controller → model → view) vía HTTP. Las system specs (Capybara) dirigen un navegador real—llenando formularios, haciendo clic, verificando contenido de página. Las pruebas unitarias (model specs) son rápidas y aisladas; las pruebas de integración/sistema son más lentas pero detectan bugs de cableado. Use la pirámide de testing: muchas pruebas unitarias rápidas, menos pruebas de integración, mínimas pruebas de sistema. have_http_status verifica códigos de respuesta; visit/fill_in/click_button dirigen el navegador.

ruby
# spec/requests/users_spec.rb (integration)
require 'rails_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users'
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob' } }
      expect(response).to have_http_status(:created)
    end
  end
end

# spec/system/login_spec.rb (browser tests)
require 'rails_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'secret')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'secret'
    click_button 'Log in'
    expect(page).to have_content('Welcome')
  end
end
16

Operaciones de Archivos y Directorios

Lectura de Archivos

File.read carga todo el archivo en memoria—bien para archivos pequeños. File.foreach lee línea por línea—esencial para archivos grandes (no agota la memoria). File.open con un bloque auto-cierra el archivo (RAII). readlines devuelve un array de líneas (con saltos de línea—use chomp). binread para archivos binarios. Siempre verifique File.exist? antes de leer si el archivo podría no existir, o rescue Errno::ENOENT.

ruby
# Read entire file
content = File.read("data.txt")

# Read line by line (memory efficient)
File.foreach("large.txt") do |line|
  puts line.chomp  # chomp removes trailing newline
end

# Read all lines into array
lines = File.readlines("data.txt")

# With a block (auto-closes)
File.open("data.txt") do |f|
  f.each_line { |line| puts line }
end

# Read with options
File.read("data.txt", encoding: "utf-8")
File.binread("image.png")  # binary mode

# Check existence
File.exist?("data.txt")
File.file?("data.txt")   # is it a regular file?
File.directory?("path")  # is it a directory?

Escritura de Archivos

File.write es la forma más simple de escribir un archivo (sobrescribe por defecto). Use mode: 'a' para añadir. File.open con un bloque asegura que el archivo se cierre incluso si ocurre una excepción. puts añade un salto de línea; write no. << es un alias de write (común en Ruby). Para logs, abra una vez y escriba múltiples veces (con búfer por rendimiento). Siempre cierre archivos o use la forma de bloque para evitar fugas de recursos.

ruby
# Write (overwrites)
File.write("output.txt", "Hello, World!")

# Append
File.write("output.txt", "More text\n", mode: "a")

# With a block (buffered, auto-closes)
File.open("log.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
  f << "appended"  # << is an alias for write
end

# Binary write
File.binwrite("data.bin", binary_data)

# Modes: r (read), w (write/truncate), a (append),
#        r+ (read/write), w+ (read/write/truncate),
#        b (binary, Windows)

# Flush buffer
f = File.open("log.txt", "w")
f.write("data")
f.flush  # write without closing
f.close

Operaciones de Directorio

Dir.glob con patrones encuentra archivos (** para recursivo). FileUtils proporciona operaciones de archivo robustas: mkdir_p crea directorios anidados, cp_r copia recursivamente, rm_rf elimina forzadamente (¡tenga cuidado!). Dir.chdir cambia el directorio de trabajo (use la forma de bloque para cambiar temporalmente). Dir.entries incluye . y ..; glob no. Prefiera FileUtils sobre operaciones manuales de File para seguridad multiplataforma.

ruby
require 'fileutils'

# List directory contents
Dir.entries(".")  # [".", "..", "file.txt", ...]
Dir.glob("*.rb")  # ["script.rb", "test.rb"]
Dir.glob("**/*.rb")  # recursive
Dir.glob("src/**/*.{rb,erb}")  # multiple extensions

# Create directories
Dir.mkdir("new_dir")
Dir.mkdir("nested/path") rescue Errno::ENOENT  # fails if parent missing
FileUtils.mkdir_p("nested/path/deep")  # creates all parents

# Remove
Dir.rmdir("empty_dir")  # only works if empty
FileUtils.rm_rf("dir")  # recursive force (dangerous!)

# Copy/Move
FileUtils.cp("a.txt", "b.txt")
FileUtils.mv("old.txt", "new.txt")
FileUtils.cp_r("src_dir", "dest_dir")  # recursive

# Change directory
Dir.chdir("/tmp") { puts Dir.pwd }  # temporarily
Dir.chdir("/tmp")  # permanently for process

# Check if directory
File.directory?("path")
File.exist?("path")

Pathname y Tempfile

Pathname es un wrapper orientado a objetos para rutas de archivos—más limpio que manipulación de cadenas. Proporciona dirname, basename, extname, join y verificaciones de archivo como métodos. Tempfile crea archivos temporales que se eliminan automáticamente (use la forma de bloque). Dir.mktmpdir crea directorios temporales. Use estos para manejo limpio y seguro de rutas y archivos temporales. Pathname compone rutas de forma segura entre plataformas (maneja / vs \).

ruby
require 'pathname'
require 'tempfile'

# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname   # #<Pathname:/home/user/docs>
path.basename  # #<Pathname:file.txt>
path.extname   # ".txt"
path.parent    # #<Pathname:/home/user/docs>
path.join("sub", "file.rb")  # /home/user/docs/sub/file.rb

path.exist?
path.directory?
path.file?
path.readable?

# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
  f.write("temporary data")
  f.rewind
  puts f.read
end  # file deleted after block

# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink  # delete

# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
  # work in dir
end  # dir deleted after

CSV y JSON

CSV y JSON están integrados en la biblioteca estándar de Ruby. CSV.foreach lee fila por fila (eficiente en memoria); CSV.read carga todo. headers: true trata la primera fila como nombres de columna. JSON.parse convierte JSON a hashes/arrays de Ruby; to_json serializa objetos Ruby. symbolize_names da claves de símbolo (más limpio). Para YAML, use require 'yaml' y YAML.load_file. Estos son esenciales para intercambio de datos en scripts de Ruby y apps web.

ruby
require 'csv'
require 'json'

# CSV reading
CSV.foreach("data.csv", headers: true) do |row|
  puts row['name']  # access by header
  puts row[0]       # access by index
end

# CSV reading all at once
rows = CSV.read("data.csv", headers: true)
rows.first['name']  # first row's name

# CSV writing
CSV.open("output.csv", "w") do |csv|
  csv << ["name", "age"]  # header
  csv << ["Alice", 30]
  csv << ["Bob", 25]
end

# JSON
data = { name: "Alice", age: 30 }
json = data.to_json  # '{"name":"Alice","age":30}'
parsed = JSON.parse(json)  # {"name"=>"Alice", "age"=>30}

# JSON with symbols
JSON.parse(json, symbolize_names: true)  # {name: "Alice", age: 30}

# Read/write JSON files
File.write("data.json", data.to_json)
loaded = JSON.parse(File.read("data.json"))
17

Codificación e Internos de Cadenas

Codificación de Cadenas

Las cadenas de Ruby llevan su codificación (usualmente UTF-8). force_encoding reinterpretará bytes como una codificación diferente (sin conversión—use cuando sepa que los bytes ya están en esa codificación). encode realmente convierte entre codificaciones. valid_encoding? verifica si los bytes son válidos para la codificación de la cadena. Los problemas de codificación causan el temido Encoding::CompatibilityError. Siempre conozca la codificación de sus datos; por defecto UTF-8.

ruby
# Ruby strings have an encoding
"hello".encoding  # #<Encoding:UTF-8>
"café".encoding   # #<Encoding:UTF-8>

# Default external encoding (for file I/O)
Encoding.default_external  # #<Encoding:UTF-8>

# Force an encoding (reinterprets bytes, doesn't convert)
bytes = "café".bytes  # [99, 97, 102, 195, 169]
latin1 = bytes.pack("C*").force_encoding("ISO-8859-1")
# "caf" + é (as single byte)

# Convert encoding (transcodes)
utf8 = "café"
latin1 = utf8.encode("ISO-8859-1")  # converts
back = latin1.encode("UTF-8")        # converts back

# Check if valid
"abc".valid_encoding?  # true
"\xff".valid_encoding?  # false (invalid byte)

# Common encodings: UTF-8, ASCII, ISO-8859-1, Windows-1252
# Always use UTF-8 unless you have a specific reason

Conversión de Codificación y E/S

La E/S de archivos usa Encoding.default_external para lectura. Especifique codificación por archivo con la opción encoding:. La sintaxis 'source:target' (ISO-8859-1:UTF-8) lee en la codificación fuente y convierte al objetivo. Establecer default_internal hace que Ruby auto-converta todas las cadenas leídas a esa codificación. Para apps web, todo debería ser UTF-8. Al procesar datos heredados, especifique codificaciones explícitamente para evitar corrupción.

ruby
# Read a file with specific encoding
content = File.read("data.txt", encoding: "ISO-8859-1")
# content.encoding is ISO-8859-1

# Read and convert to UTF-8
content = File.read("data.txt", encoding: "ISO-8859-1:UTF-8")
# The "source:target" syntax converts while reading

# Write with specific encoding
File.write("output.txt", "café", encoding: "UTF-8")

# Open with encoding
File.open("data.txt", "r:ISO-8859-1") do |f|
  f.read  # ISO-8859-1 encoded string
end

# Convert while opening
File.open("data.txt", "r:ISO-8859-1:UTF-8") do |f|
  f.read  # UTF-8 encoded string
end

# Set default encodings
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

Métodos de Cadena en Profundidad

Las cadenas de Ruby tienen métodos ricos. Inspección (length, include?, start_with?) verifica propiedades. Los métodos de transformación devuelven nuevas cadenas (las cadenas son mutables en Ruby, pero estos no mutan). El acceso a substring usa [start, length] o rangos. sub reemplaza la primera coincidencia; gsub reemplaza todas (admite regex y bloques). split/join convierten entre cadenas y arrays. Nota: el pragma frozen_string_literal de Ruby 3.0+ hace las cadenas inmutables por rendimiento—use << o + para construirlas.

ruby
s = "Hello, World"

# Inspection
s.length    # 12
s.empty?    # false
s.include?("World")  # true
s.start_with?("Hello")  # true
s.end_with?("World")  # true

# Transformation (return new string)
s.upcase     # "HELLO, WORLD"
s.downcase   # "hello, world"
s.capitalize # "Hello, world"
s.swapcase   # "hELLO, wORLD"
s.reverse    # "dlroW ,olleH"
s.strip      # remove leading/trailing whitespace
s.chomp(",") # remove trailing substring
s.chop       # remove last char

# Substrings
s[0, 5]      # "Hello" (start, length)
s[7..11]     # "World" (range)
s[-5..]      # "World" (negative index)

# Replace
s.sub("World", "Ruby")    # first match
s.gsub("o", "0")          # all matches
s.gsub(/\w+/) { |w| w.upcase }  # with block

# Split/Join
"a,b,c".split(",")  # ["a", "b", "c"]
["a", "b"].join("-")  # "a-b"

Cadenas Congeladas y Rendimiento

frozen_string_literal: true (comentario mágico en la cima del archivo) hace que todos los literales de cadena sean inmutables—esto es una optimización de rendimiento (las cadenas congeladas pueden compartir memoria) y previene bugs de mutación accidental. Para construir cadenas, use << (añadir in situ, O(n)) no += (crea nueva cadena cada vez, O(n²)). join es lo más limpio para arrays. StringIO actúa como un archivo pero escribe a una cadena—útil para construir salida compleja. Ruby 3.x fomenta cadenas congeladas por defecto.

ruby
# frozen_string_literal: true  (at top of file)
# Makes all string literals in the file frozen (immutable)

# Mutable vs frozen
s = "hello"
s << " world"  # "hello world" (mutates)

f = "hello".freeze
# f << " world"  # Error: can't modify frozen String

# Why freeze? Performance: frozen strings share memory
# "abc".freeze is the same object everywhere
"a".freeze.equal?("a".freeze)  # true (same object)

# Building strings efficiently
# BAD: creates many intermediate strings
result = ""
items.each { |i| result += i.to_s }  # O(n²)

# GOOD: use << or join
result = ""
items.each { |i| result << i.to_s }  # O(n)
result = items.map(&:to_s).join  # cleanest

# StringIO for buffered building
require 'stringio'
io = StringIO.new
io << "line 1\n"
io << "line 2\n"
result = io.string

Símbolos vs Cadenas

Los símbolos (:name) son identificadores singleton inmutables—solo existe un :foo en memoria, siempre. Las cadenas son datos de texto mutables con múltiples instancias. Use símbolos para claves de hash (verificaciones de igualdad más rápidas), nombres de métodos y valores tipo enum. Use cadenas para texto real. Los símbolos son ligeramente más rápidos para claves de hash y comparación. En Ruby moderno (2.2+), los símbolos pueden ser recolectados por el garbage collector, así que la vieja preocupación de 'fuga de memoria de símbolos' desapareció. Rails usa símbolos extensivamente para claves y valores de estado.

ruby
# Symbol: immutable, reusable identifier
:hello
:world
status = :active

# String: mutable, can have many instances
"hello"
"world"
status = "active"

# Key difference: identity
"hello".equal?("hello")  # false (different objects)
:hello.equal?(:hello)    # true (same object, singleton)

# Memory: symbols are singletons
1000.times { :foo }  # one :foo object
1000.times { "foo" }  # 1000 "foo" objects (unless frozen)

# Use symbols for:
# - Hash keys (faster comparison)
# - Method names (send(:method_name))
# - Enum-like values (:active, :pending, :closed)
# - Identifiers (not text data)

# Use strings for:
# - Text data (names, content)
# - Things that change

# Conversion
:hello.to_s  # "hello"
"hello".to_sym  # :hello
"hello world".to_sym  # :"hello world" (valid but ugly)
18

Módulos y Mixins

Fundamentos de Módulos

Los módulos sirven dos propósitos: namespacing (agrupar código relacionado, prevenir choques de nombres) y mixins (compartir comportamiento sin herencia). Los métodos de módulo (def self.method) se llaman en el módulo. Los métodos de instancia (def method) son para mezclar en clases. Los módulos no pueden instanciarse. Use módulos para namespaciar clases (MyApp::User) y para organizar constantes y funciones de utilidad. Esta es la alternativa de Ruby a la herencia múltiple.

ruby
# Module: a namespace + collection of methods
module MathUtils
  PI = 3.14159

  def self.circle_area(radius)  # module method
    PI * radius ** 2
  end

  def square(x)  # instance method (for mixins)
    x * x
  end
end

# Access constants and module methods
MathUtils::PI  # 3.14159
MathUtils.circle_area(5)  # 78.54

# Namespacing classes
module MyApp
  class User
    # MyApp::User
  end
end

# Prevent instantiation (modules can't be instantiated)
# MathUtils.new  # NoMethodError

Include vs Extend vs Prepend

Tres formas de mezclar módulos: include (añade métodos de instancia, va debajo de la clase en la búsqueda), extend (añade métodos de clase), prepend (añade métodos de instancia, va encima de la clase—puede envolver/sobrescribir). prepend es potente para hooks before/after (llame super para invocar el original). Búsqueda de métodos: prepend → class → include → superclass. Use include para mixins normales, prepend cuando necesite envolver métodos existentes, extend para funcionalidad a nivel de clase.

ruby
module Greetable
  def greet
    "Hello from #{self.class}"
  end
end

class User
  include Greetable  # adds as instance methods
end
User.new.greet  # "Hello from User"

class Service
  extend Greetable  # adds as class methods
end
Service.greet  # "Hello from Service"

# Method lookup order:
# prepend → class → include → super
module Logging
  def save
    puts "before save"
    super  # calls the original save
    puts "after save"
  end
end

class Record
  prepend Logging  # Logging#save runs first
  def save; puts "saving"; end
end
Record.new.save
# before save / saving / after save

Mixin Enumerable

Incluir Enumerable y definir #each le da map, select, reduce, sort, min, max y 40+ métodos más—este es el protocolo iterador de Ruby. Incluir Comparable y definir <=> le da <, >, ==, between?, clamp y soporte de sort. Estos mixins son por qué las colecciones de Ruby son tan potentes. Cualquier clase que represente una colección o tenga un ordenamiento natural debería incluir estos. Es composición sobre herencia.

ruby
# Include Enumerable + define #each = 50+ methods free
class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each
    @songs.each { |song| yield song }
  end
end

playlist = Playlist.new(["Song A", "Song B", "Song C"])
playlist.map { |s| s.upcase }   # ["SONG A", "SONG B", "SONG C"]
playlist.select { |s| s.include?("A") }  # ["Song A"]
playlist.reduce(:+)            # "Song ASong BSong C"
playlist.sort                  # ["Song A", "Song B", "Song C"]
playlist.first(2)              # ["Song A", "Song B"]
playlist.include?("Song B")    # true
playlist.count                 # 3

# Comparable: define <=> for natural ordering
class Temperature
  include Comparable
  attr_reader :celsius

  def initialize(c) @celsius = c end
  def <=>(other) celsius <=> other.celsius end
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2   # true (uses <=>)
t1 == t2  # false

Métodos Singleton y Métodos de Clase

Los métodos singleton pertenecen a un objeto específico. Los métodos de clase son solo métodos singleton en el objeto clase. 'class << self' abre la clase singleton (eigenclass) para definir múltiples métodos de clase limpiamente. El patrón singleton usa una variable de clase para mantener una instancia. Entender las clases singleton es clave para el modelo de objetos de Ruby—cada objeto tiene una clase singleton que contiene sus métodos únicos. Esto habilita personalización por objeto y metaprogramación.

ruby
# Singleton method: defined on one object only
str = "hello"
def str.shout
  upcase + "!"
end
str.shout  # "HELLO!"
# "world".shout  # NoMethodError (only str has it)

# Class methods are singleton methods on the class
class Calculator
  def self.add(a, b)  # singleton method on Calculator
    a + b
  end
end
Calculator.add(1, 2)  # 3

# Singleton class (eigenclass): where singleton methods live
class Calculator
  class << self  # opens the singleton class
    def multiply(a, b) a * b end
    def divide(a, b) a / b end
  end
end
Calculator.multiply(2, 3)  # 6

# Singleton pattern
class Logger
  @instance = Logger.new
  class << self
    attr_reader :instance
  end
end
Logger.instance.equal?(Logger.instance)  # true

Refinements (Monkey Patching con Ámbito)

Los Refinements (Ruby 2.1+) permiten monkey patching con ámbito—añada métodos a clases existentes pero solo donde 'using' el refinement explícitamente. Esto es más seguro que el monkey patching global (que puede romper otro código). Los refinements se activan por ámbito (archivo, clase, método). Son útiles para añadir métodos de conveniencia sin contaminar el namespace global. Menos comunes de lo que deberían ser debido al rendimiento y algunas peculiaridades de ámbito, pero son la forma 'correcta' de extender clases core.

ruby
# Refinement: limited monkey patching
module StringRefinements
  refine String do
    def shout
      upcase + "!!!"
    end

    def palindrome?
      downcase == downcase.reverse
    end
  end
end

# Without using: shout is undefined
# "hello".shout  # NoMethodError

# With using: refinement is active in this scope
using StringRefinements
"hello".shout        # "HELLO!!!"
"racecar".palindrome?  # true

# Scoped to file or module
class App
  using StringRefinements
  def greet
    "hi".shout  # works here
  end
end
# "hi".shout  # NoMethodError (outside App)

# Safer than global monkey patching
# Refinements only affect code that opts in
19

Bloques Procs Lambdas en Profundidad

Fundamentos de Bloques

Los bloques son closures anónimos pasados a métodos usando { } o do...end. yield invoca el bloque desde dentro del método. Los bloques pueden tomar parámetros vía |var|. Un método puede verificar si se pasó un bloque con block_given?. Los bloques son la base del patrón iterador de Ruby y los DSLs. Capturan variables del ámbito envolvente (closures).

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }      # Single-line
[1, 2, 3].each do |n|              # Multi-line
  puts n
end

# yield invokes the block
def greet
  print "Hello, "
  yield          # Calls the block
  puts "!"
end

greet { print "World" }   # Hello, World!

# yield with arguments
def calculate(a, b)
  yield(a + b)
  yield(a * b)
end

calculate(3, 4) { |result| puts "Result: #{result}" }

Procs y Lambdas

Los Procs y lambdas son objetos que envuelven bloques, permitiendo almacenamiento en variables y paso. Los Procs tienen verificación indulgente de argumentos (argumentos extra ignorados) y retornan desde el método envolvente. Las Lambdas tienen verificación estricta de argumentos y retornan solo desde sí mismas. Use Procs para flexibilidad (como métodos que aceptan bloques), lambdas para funciones anónimas con comportamiento predecible. La sintaxis ->() {} (stabby lambda) es concisa para funciones de una línea.

ruby
# Proc: object wrapping a block
my_proc = Proc.new { |x| puts x * 2 }
my_proc.call(5)         # 10
my_proc.(5)             # 10 (shorthand)
my_proc[5]              # 10 (another shorthand)

# Lambda: stricter Proc
my_lambda = lambda { |x| puts x * 2 }
my_lambda = ->(x) { puts x * 2 }   # Stabby lambda

# Difference 1: argument checking
my_proc.call(1, 2, 3)    # OK (ignores extra args)
my_lambda.call(1, 2, 3)  # ArgumentError (wrong number of args)

# Difference 2: return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # Never reached
end

def lambda_test
  l = lambda { return 1 }
  l.call    # Returns 1 to the lambda, not the method
  return 2  # Reached
end

Objetos Method

El método method() devuelve un objeto Method envolviendo un método existente. El operador & convierte un Method o Proc a un bloque (y viceversa). Symbol#to_proc convierte :upcase a { |x| x.upcase }, habilitando la concisa sintaxis &:symbol. Esto es Ruby idiomático para operaciones de bloque cortas. Los objetos Method están vinculados a su receptor, así que recuerdan self cuando se pasan.

ruby
# Convert method to Proc with method()
class Calculator
  def add(a, b) a + b end
  def multiply(a, b) a * b end
end

calc = Calculator.new
add_proc = calc.method(:add)
puts add_proc.call(2, 3)   # 5

# Pass method as block with & operator
def apply_op(a, b, op)
  op.call(a, b)
end

puts apply_op(2, 3, calc.method(:add))       # 5
puts apply_op(2, 3, calc.method(:multiply))  # 6

# Symbol#to_proc
words = ["hello", "world"]
upcased = words.map(&:upcase)   # ["HELLO", "WORLD"]
# Equivalent to: words.map { |w| w.upcase }

# Common with &:map(&:to_i), select(&:positive?), sort_by(&:length)

Closures y Variables

Los closures capturan variables por referencia, no por valor. Múltiples closures pueden compartir estado (el ejemplo del contador). Las variables block-local (declaradas después de ; en parámetros) sombrean variables externas sin modificarlas. Esto habilita patrones funcionales como acumuladores, generadores y memoización. Tenga cuidado: los closures que mantienen referencias pueden causar fugas de memoria si no se liberan. Use este patrón para encapsulación de estado privado.

ruby
# Blocks/Procs/Lambdas capture variables (closures)
counter = 0
increment = lambda { counter += 1 }

increment.call
increment.call
puts counter   # 2

# Multiple closures sharing state
def make_counters
  count = 0
  [
    lambda { count += 1 },
    lambda { count },
    lambda { count = 0 }
  ]
end

inc, get, reset = make_counters
inc.call; inc.call
puts get.call   # 2
reset.call
puts get.call   # 0

# Block-local variables (shadow outer)
x = 10
[1, 2, 3].each do |y; x|   # x is block-local
  x = y * 2
end
puts x   # 10 (unchanged)

Iteradores Personalizados

Incluir Enumerable e implementar each le da a su clase todos los métodos iteradores (map, select, reduce, sort, etc.) gratis. Pase el bloque con &block y llámelo, o use yield. El operador & convierte un bloque a un Proc y viceversa. Esta es la forma idiomática de hacer colecciones personalizadas iterables. Implemente each para iteración hacia adelante; añada reverse_each para bidireccional.

ruby
class Tree
  include Enumerable

  def initialize(value, children = [])
    @value = value
    @children = children
  end

  def each(&block)
    block.call(@value)
    @children.each { |child| child.each(&block) }
  end
end

tree = Tree.new(1, [
  Tree.new(2, [Tree.new(4), Tree.new(5)]),
  Tree.new(3)
])

tree.each { |v| puts v }            # 1 2 4 5 3
puts tree.map { |v| v * 2 }.inspect # [2, 4, 8, 10, 6]
puts tree.select(&:even?).inspect   # [2, 4]
puts tree.reduce(:+)                # 15

# Including Enumerable gives you map, select, reduce, etc.
20

Metaprogramación

Métodos Dinámicos

method_missing intercepta llamadas a métodos no definidos, habilitando despacho dinámico. Siempre sobrescriba respond_to_missing? para coincidir. define_method crea métodos en tiempo de ejecución, útil para generar métodos similares (como find_by_* de ActiveRecord). instance_variable_get/set acceden a variables de instancia por nombre. Use metaprogramación con moderación—hace el código más difícil de entender y depurar. Prefiera definiciones explícitas cuando sea posible.

ruby
class Person
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  # Method missing: catch undefined method calls
  def method_missing(name, *args, &block)
    if name.to_s =~ /^(.*)_with_prefix$/
      attr_name = $1
      "PREFIX_#{send(attr_name)}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s =~ /^(.*)_with_prefix$/ || super
  end
end

p = Person.new("Alice", 30)
puts p.name_with_prefix   # PREFIX_Alice

# Define methods dynamically
class Person
  [:home, :work, :mobile].each do |type|
    define_method("#{type}_phone") do
      instance_variable_get("@#{type}_phone")
    end

    define_method("#{type}_phone=") do |value|
      instance_variable_set("@#{type}_phone", value)
    end
  end
end

Clases Abiertas y Monkey Patching

Las clases de Ruby son abiertas: puede añadir métodos a cualquier clase, incluyendo las integradas como String. Esto es potente pero peligroso (el monkey patching puede romper otro código). Los Refinements (Ruby 2.1+) proporcionan monkey patches con ámbito: se aplican solo dentro de archivos/clases que usan el módulo. Prefiera refinements sobre monkey patches globales para metaprogramación más segura. Documente los patches claramente y evite cambiar comportamiento core.

ruby
# Reopen existing classes (monkey patching)
class String
  def shout
    upcase + "!"
  end

  def word_count
    split.size
  end
end

puts "hello world".shout       # HELLO WORLD!
puts "one two three".word_count # 3

# Refinements: scoped monkey patches
module ShoutRefinement
  refine String do
    def shout
      upcase + "!"
    end
  end
end

class MyClass
  using ShoutRefinement

  def greet(name)
    "hello #{name}".shout   # Works here
  end
end

# "test".shout  # NoMethodError (refinement not active)

Hooks y Callbacks

Ruby proporciona hooks de ciclo de vida: inherited (subclase creada), included (módulo incluido), prepended (módulo prependido), method_added (método definido), method_removed, method_undefined. Estos habilitan a los frameworks reaccionar a cambios de clase automáticamente. ActiveRecord los usa para rastrear atributos, Rails los usa para routing. Sobrescriba hooks como métodos de clase (self.inherited) para eventos a nivel de clase, métodos de instancia para eventos de método.

ruby
class Observable
  # Hook: called when class is subclassed
  def self.inherited(subclass)
    puts "#{subclass} inherits from #{self}"
  end

  # Hook: called when a module is included
  def self.included(base)
    puts "#{self} included in #{base}"
  end

  # Hook: called when a method is added
  def method_added(name)
    puts "Added method: #{name}"
  end

  # Hook: called when method is undefined
  def method_removed(name)
    puts "Removed method: #{name}"
  end
end

class Child < Observable
  def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method

# Other hooks: method_undefined, extended, prepended

eval y Binding

eval ejecuta una cadena como código Ruby, peligroso si la entrada no es confiable (inyección de código). binding captura el contexto de ejecución actual (variables, self) para eval posterior. class_eval ejecuta código en un contexto de clase (define métodos). instance_eval cambia self al receptor. Use estos para DSLs y generación de código, pero evite eval en entrada de usuario. Prefiera bloques y define_method para código dinámico. Siempre sane la entrada si eval es inevitable.

ruby
# eval: execute a string as Ruby code
result = eval("1 + 2 * 3")
puts result   # 7

# eval with binding (context)
def eval_in_context(code)
  x = 10
  binding   # Returns a Binding object capturing local variables
end

b = eval_in_context("binding")
puts eval("x * 2", b)   # 20

# class_eval: execute code in class context
String.class_eval do
  def palindrome?
    self == reverse
  end
end

puts "racecar".palindrome?   # true

# instance_eval: execute in instance context
"hello".instance_eval do
  puts length   # 5 (self is the string)
end

# NEVER eval user input (security risk)

Reflexión e Introspección

Ruby proporciona reflexión rica: methods lista todos los métodos, instance_variables lista variables de instancia, ancestors muestra la cadena de herencia. parameters revela los nombres y tipos de parámetros de un método. respond_to? verifica si un objeto responde a un método. Use reflexión para depuración, serialización (inspeccionar atributos) y construcción de herramientas genéricas (como ORMs). Evite el uso excesivo de reflexión—omite la seguridad de tipos y hace el código más difícil de seguir.

ruby
class User
  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def save; end
  def valid?; true; end
end

u = User.new("Alice", "[email protected]")

# Inspect methods
puts u.methods - Object.methods    # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false)       # Only this class's methods
puts User.instance_methods(false)  # [:save, :valid?, ...]

# Inspect variables
puts u.instance_variables.inspect   # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice

# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters

# Check if responds to method
puts u.respond_to?(:save)   # true
puts u.is_a?(User)          # true
21

Testing (RSpec/Minitest)

Fundamentos de RSpec

RSpec es un framework de testing estilo BDD. describe agrupa pruebas relacionadas, context agrupa por condición, it define una sola prueba. let crea variables perezosas memoizadas. expect(...).to matcher es la sintaxis de aserción. Matchers comunes: eq, be_valid, include, raise_error. subject + is_expected reduce código repetitivo. Ejecute con rspec (todas) o rspec path/to/spec (específica). Use factories (FactoryBot) en lugar de fixtures para datos de prueba.

ruby
# spec/spec_helper.rb
require 'rspec'
require_relative '../lib/my_app'

# spec/models/user_spec.rb
require 'spec_helper'

RSpec.describe User do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: 'Alice', email: '[email protected]') }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq('Alice')
    end
  end

  describe '#valid?' do
    context 'with valid attributes' do
      it 'is valid' do
        expect(user).to be_valid
      end
    end

    context 'without name' do
      let(:user) { User.new(email: '[email protected]') }
      it 'is invalid' do
        expect(user).not_to be_valid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end
  end

  # Multiple examples
  describe '#age' do
    subject { user.age }
    it { is_expected.to be >= 0 }
  end
end

# Run: rspec spec/models/user_spec.rb
# Run all: rspec

Mocks y Stubs de RSpec

Los Stubs (allow) reemplazan valores de retorno de métodos; los mocks (expect) verifican que se llamó el método. double crea un test double (objeto falso). and_return establece valores de retorno, and_raise simula errores, with establece argumentos esperados. Use mocks con moderación—el exceso de mocks hace las pruebas frágiles. Pruebe comportamiento, no implementación. Prefiera objetos reales cuando sea factible; use mocks para servicios externos (APIs, email, pasarelas de pago).

ruby
RSpec.describe PaymentProcessor do
  let(:gateway) { double('PaymentGateway') }
  let(:processor) { PaymentProcessor.new(gateway) }

  describe '#charge' do
    it 'charges the gateway' do
      # Stub: replace method return value
      allow(gateway).to receive(:charge).and_return(success: true)

      result = processor.charge(100)
      expect(result[:success]).to be true
    end

    it 'raises on gateway error' do
      allow(gateway).to receive(:charge)
        .and_raise(GatewayError, 'Network failure')

      expect { processor.charge(100) }
        .to raise_error(GatewayError, /Network/)
    end

    it 'verifies the gateway was called' do
      # Mock: expect method to be called
      expect(gateway).to receive(:charge).with(100)
      processor.charge(100)
    end

    it 'receives multiple calls' do
      allow(gateway).to receive(:charge).and_return(true, false, true)
      expect(gateway.charge(1)).to be true
      expect(gateway.charge(2)).to be false
    end
  end
end

Minitest

Minitest es la biblioteca de testing por defecto de Ruby (incluida con Ruby). Admite sintaxis estilo Unit (assert/refute) y estilo Spec (must_equal). setup se ejecuta antes de cada prueba. Aserciones: assert_equal, assert_nil, assert_raises, assert_includes. Minitest es más rápido que RSpec y tiene menos dependencias. Use Minitest::Mock para mocking, o la gem Mocha para más características. Rails usa Minitest por defecto (ActiveSupport::TestCase).

ruby
# test/test_helper.rb
require 'minitest/autorun'
require_relative '../lib/my_app'

# test/models/user_test.rb
require 'test_helper'

class UserTest < Minitest::Test
  def setup
    @user = User.new(name: 'Alice', email: '[email protected]')
  end

  def test_name
    assert_equal 'Alice', @user.name
  end

  def test_valid_with_attributes
    assert @user.valid?
  end

  def test_invalid_without_name
    @user.name = nil
    refute @user.valid?
    assert_includes @user.errors[:name], "can't be blank"
  end

  def test_raises_on_error
    assert_raises(ArgumentError) { User.new! }
  end
end

# Spec-style Minitest
class UserSpec < Minitest::Spec
  let(:user) { User.new(name: 'Alice') }

  it 'has a name' do
    _(user.name).must_equal 'Alice'
  end
end

# Run: rake test
# Run specific: ruby -Itest test/models/user_test.rb

Datos de Prueba y Factories

FactoryBot genera objetos de prueba con valores predeterminados, traits para variaciones y asociaciones. create persiste a DB; build no. Los Traits se componen (factory :admin_with_posts, traits: [:admin, :with_posts]). Las factories son más flexibles que las fixtures pero más lentas. Las fixtures (YAML) son más rápidas pero menos flexibles. Elija según necesidades: fixtures para datos simples, factories para relaciones complejas. Evite factories con demasiados traits—manténgalas enfocadas.

ruby
# FactoryBot (gem 'factory_bot')
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Alice' }
    email { '[email protected]' }
    age { 30 }

    trait :admin do
      role { 'admin' }
    end

    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
    factory :user_with_posts, traits: [:with_posts]
  end
end

# Usage in specs
let(:user) { create(:user) }            # Saved to DB
let(:admin) { create(:admin_user) }
let(:user_with_posts) { create(:user_with_posts) }
let(:unsaved_user) { build(:user) }     # Not saved

# Fixtures (alternative)
# test/fixtures/users.yml
# alice:
#   name: Alice
#   email: [email protected]

# In tests: users(:alice)

Pruebas de Integración y Sistema

Las request specs prueban el stack completo (routing, controllers, models) vía HTTP. Las system specs (Capybara) dirigen un navegador real, probando interacciones JavaScript. Use request specs para endpoints de API, system specs para flujos de usuario. have_http_status verifica códigos de respuesta. visit/fill_in/click_button simulan acciones de usuario. Las pruebas de sistema son más lentas pero detectan bugs de integración. Ejecute pruebas de sistema con js: true para páginas impulsadas por JavaScript.

ruby
# spec/requests/users_spec.rb (integration)
require 'spec_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users', as: :json
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob', email: '[email protected]' } }
      expect(response).to have_http_status(201)
      expect(User.last.name).to eq('Bob')
    end
  end
end

# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'password123')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password123'
    click_button 'Log In'
    expect(page).to have_content('Welcome')
  end
end
22

Threads y Fibers

Fundamentos de Threads

Los threads en Ruby (MRI) son green threads programados por la VM—no se ejecutan verdaderamente en paralelo debido al GIL (Global Interpreter Lock). Sin embargo, los threads son útiles para concurrencia de E/S (red, operaciones de archivo). join espera a que un thread complete. value recupera el valor de retorno. Para paralelismo limitado por CPU, use múltiples procesos (fork, Sidekiq) o JRuby (sin GIL). Siempre haga join de los threads para evitar que se maten cuando el thread principal sale.

ruby
# Create and run threads
threads = (1..3).map do |i|
  Thread.new(i) do |n|
    puts "Thread #{n} started"
    sleep(1)
    puts "Thread #{n} finished"
  end
end

# Wait for all threads
threads.each(&:join)
puts "All done"

# Thread with return value
thread = Thread.new { 1 + 2 }
result = thread.value   # 3 (waits for completion)

# Thread status
thread = Thread.new { sleep(1) }
puts thread.status   # 'run', 'sleep', false (finished), nil (error)
thread.join

# Current thread
Thread.current
Thread.list   # All threads
Thread.main   # Main thread

Sincronización de Threads

Mutex.synchronize asegura que solo un thread ejecute un bloque a la vez, previniendo race conditions. Queue es un FIFO seguro para subprocesos—los productores push, los consumidores pop (se bloquea si está vacío). ConditionVariable coordina threads: wait libera el mutex y duerme, signal/broadcast despierta threads en espera. Siempre use sincronización para estado mutable compartido. Los interbloqueos ocurren cuando los threads se esperan mutuamente—adquiera locks en un orden consistente.

ruby
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new

threads = 10.times.map do
  Thread.new do
    1000.times do
      mutex.synchronize { counter += 1 }
    end
  end
end
threads.each(&:join)
puts counter   # 10000 (without mutex, would be less)

# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new

producer = Thread.new do
  5.times { |i| queue << "item #{i}" }
  queue << :done
end

consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    puts "Processed: #{item}"
  end
end

[producer, consumer].each(&:join)

# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.new

Fibers

Las fibers son threads ligeros cooperativos: ceden control manualmente en lugar de ser preemptados. resume inicia/reanuda una fiber; Fiber.yield la pausa y devuelve un valor. Las fibers son útiles para generadores, evaluación perezosa y análisis de máquinas de estados. A diferencia de los threads, solo una fiber se ejecuta a la vez, así que no se necesita sincronización. Las fibers son más baratas que los threads pero no pueden usar múltiples núcleos. Úselas para multiplexación de E/S (EventMachine, Async).

ruby
# Fiber: cooperative concurrency (manual scheduling)
fiber = Fiber.new do
  puts "Fiber started"
  Fiber.yield "first yield"
  puts "Fiber resumed"
  Fiber.yield "second yield"
  puts "Fiber ending"
  "fiber done"
end

puts fiber.resume   # "Fiber started", returns "first yield"
puts fiber.resume   # "Fiber resumed", returns "second yield"
puts fiber.resume   # "Fiber ending", returns "fiber done"

# Generator pattern with Fiber
def fibonacci
  Fiber.new do
    a, b = 0, 1
    loop do
      Fiber.yield a
      a, b = b, a + b
    end
  end
end

fib = fibonacci
10.times { print fib.resume, " " }
# 0 1 1 2 3 5 8 13 21 34

Ruby Async y Concurrente

La gem async proporciona E/S asíncrona moderna usando Fibers bajo el capó, habilitando código de red de alta concurrencia. concurrent-ruby proporciona abstracciones seguras para subprocesos: Future (resultado async), Promise (encadenable), thread pools y variables atómicas. Use async para trabajo limitado por E/S (HTTP, bases de datos), thread pools para trabajo limitado por CPU. Siempre apague los pools para evitar cuelgues. Estas gems sortean el GIL de MRI para concurrencia práctica.

ruby
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'

Async do
  internet = Async::HTTP::Internet.new

  # Run requests concurrently
  tasks = 3.times.map do |i|
    Async do
      response = internet.get("https://httpbin.org/delay/#{i}")
      puts "Request #{i}: #{response.status}"
    end
  end

  tasks.each(&:wait)
end

# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'

# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value   # 42 (blocks until ready)

# Promise chain
Concurrent::Promise.execute { 1 }
  .then { |v| v + 1 }
  .then { |v| v * 2 }
  .then { |v| puts v }   # 4

# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
  pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdown

Ractors (Ruby 3.0+)

Los Ractors (Ruby 3.0+) proporcionan paralelismo verdadero evitando el GIL. Cada Ractor tiene su propio heap, así que no hay data races. La comunicación es vía mensajes (send/receive, take). Los objetos enviados se copian (o se mueven con Ractor.move) para mantener aislamiento. Los objetos congelados pueden compartirse. Los Ractors son el futuro del Ruby paralelo pero tienen restricciones: la mayoría de las gems aún no son Ractor-safe. Úselos para computación paralela limitada por CPU donde el aislamiento es aceptable.

ruby
# Ractor: true parallelism (no GIL)
# Ruby 3.0+ feature for thread-safe parallel execution

ractor = Ractor.new do
  value = receive    # Receive from main
  value * 2
end

ractor.send(21)
puts ractor.take     # 42

# Parallel computation
ractors = (1..4).map do |i|
  Ractor.new(i) do |n|
    sleep(1)
    n * n
  end
end

results = Ractor.select(*ractors)
# Or: ractors.map(&:take)  # [1, 4, 9, 16]

# Ractor restrictions:
# - Cannot share mutable objects
# - Communication only via messages
# - Copy or move semantics for sending

# Shared frozen objects are OK
SHARED = Ractor.new { [1, 2, 3].freeze }
# Multiple ractors can read SHARED safely
23

Codificación y E/S

Codificación de Cadenas

Cada cadena de Ruby tiene una codificación (predeterminado UTF-8). bytesize es el conteo de bytes; length es el conteo de caracteres (diferente para codificaciones multibyte). force_encoding cambia la etiqueta de codificación sin convertir bytes (use cuando conozca la codificación real). encode convierte entre codificaciones. Siempre verifique valid_encoding? antes de procesar datos externos. Establezca la codificación del archivo fuente con el comentario mágico # encoding: utf-8 (aunque UTF-8 es predeterminado en Ruby 2.0+).

ruby
# Ruby strings have encodings
s = "hello"
puts s.encoding              # <Encoding:UTF-8>
puts s.bytesize              # 5
puts s.length               # 5

# Multibyte characters
japanese = "こんにちは"
puts japanese.encoding       # UTF-8
puts japanese.bytesize       # 15 (3 bytes per char)
puts japanese.length         # 5 (5 characters)

# Force encoding (interpret bytes differently)
bytes = "caf\u00e9".force_encoding('ASCII-8BIT')
puts bytes.encoding          # ASCII-8BIT

# Encode (convert to different encoding)
utf8 = "café"
latin1 = utf8.encode('ISO-8859-1')
puts latin1.encoding         # ISO-8859-1

# Invalid bytes
bad = "\xFF\xFE".force_encoding('UTF-8')
puts bad.valid_encoding?     # false
fixed = bad.encode('UTF-8', invalid: :replace, replace: '?')

E/S de Archivos

File.read carga todo el archivo en memoria; File.foreach lee línea por línea (eficiente en memoria para archivos grandes). File.write sobrescribe; modo 'a' añade; 'r+' lee y escribe. Modo binario ('rb', 'wb') previene conversión de codificación. Siempre use la forma de bloque (File.open) para asegurar que el archivo se cierre. FileUtils proporciona operaciones de más alto nivel (cp, mv, mkdir_p). Verifique existencia con File.exist? antes de leer.

ruby
# Read entire file
content = File.read('data.txt')

# Read line by line (memory efficient)
File.foreach('large.log') do |line|
  puts line.chomp
end

# Write to file
File.write('output.txt', 'Hello, World!')

# Append
File.open('log.txt', 'a') do |f|
  f.puts "New log entry"
end

# Read/Write modes
File.open('file.txt', 'r+') do |f|
  content = f.read
  f.rewind
  f.write("Updated: " + content)
end

# Binary mode
File.open('image.png', 'rb') do |f|
  bytes = f.read
  puts bytes.bytesize
end

# File operations
File.exist?('file.txt')
File.size('file.txt')
File.mtime('file.txt')
File.delete('file.txt')
File.rename('old.txt', 'new.txt')
FileUtils.cp('a.txt', 'b.txt')   # require 'fileutils'

StringIO y Tempfile

StringIO envuelve una cadena en una interfaz tipo IO, útil para probar operaciones de archivo sin tocar disco. Tempfile crea archivos temporales que se eliminan automáticamente cuando el bloque sale (Tempfile.create) o cuando se desvinculan (Tempfile.new). Use Tempfile para datos grandes que no caben en memoria o para pasar a programas externos vía ruta. Siempre asegure limpieza con bloques ensure. StringIO es genial para pruebas unitarias de código IO.

ruby
require 'stringio'
require 'tempfile'

# StringIO: in-memory file-like object
io = StringIO.new
io.puts "Hello"
io.puts "World"
io.rewind
puts io.read   # "Hello\nWorld\n"

# Use like a file
io = StringIO.new("line1\nline2\nline3")
io.each_line { |line| puts line.chomp }

# Tempfile: auto-deleted temporary file
Tempfile.create('myapp') do |f|
  f.write('temporary data')
  f.rewind
  puts f.read
end   # File automatically deleted

# Tempfile with explicit cleanup
temp = Tempfile.new('myapp')
begin
  temp.write('data')
  temp.rewind
  # Use temp.path to pass to external programs
ensure
  temp.close
  temp.unlink
end

E/S de Red

TCPSocket/TCPServer proporcionan acceso TCP de bajo nivel. Net::HTTP es el cliente HTTP estándar (integrado). Para necesidades HTTP complejas (sesiones, cookies, reintentos), use las gems httparty o faraday. Siempre establezca use_ssl = true para HTTPS. Para HTTP de alto rendimiento, considere async-http o typhoeus. URI analiza URLs de forma segura. Maneje Net::ReadTimeout y Errno::ECONNREFUSED para código de red robusto.

ruby
require 'socket'
require 'net/http'
require 'uri'

# TCP Client
TCPSocket.open('example.com', 80) do |socket|
  socket.write("GET / HTTP/1.0\r\n\r\n")
  puts socket.read
end

# TCP Server
server = TCPServer.new(2000)
loop do
  client = server.accept
  client.puts "Hello from server"
  client.close
end

# HTTP Client
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
puts response.code          # 200
puts response.body

# HTTP POST
response = Net::HTTP.post_form(uri, key: 'value')

# HTTP with custom headers
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
response = http.request(request)

STDIO y Pipelines

$stdout, $stderr, $stdin son objetos IO globales (use las constantes STDOUT/STDERR/STDIN para los originales). Redirija reasignando globales. StringIO captura salida para testing. Open3.popen3 da control total sobre stdin/stdout/stderr de un subproceso. Open3.pipeline encadena comandos como un pipe de shell. Capture3 devuelve stdout, stderr y status. Siempre cierre stdin para señalar EOF al subproceso. Use wait.value para obtener el estado de salida.

ruby
# Standard IO
$stdout.puts "To stdout"
$stderr.puts "To stderr"
input = $stdin.gets   # Read from stdin

# Redirect IO
$stdout = File.open('log.txt', 'w')
puts "This goes to file"
$stdout = $stdout   # Restore (or use STDOUT constant)

# Capture output
captured = StringIO.new
original = $stdout
$stdout = captured
puts "Captured"
$stdout = original
puts captured.string   # "Captured\n"

# Open3: run commands with full IO control
require 'open3'

Open3.popen3('grep hello') do |stdin, stdout, stderr, wait|
  stdin.puts "hello world"
  stdin.puts "goodbye"
  stdin.close
  puts stdout.read      # "hello world"
  puts wait.value.exitstatus
end

# Pipeline
output = Open3.pipeline('ls', 'grep .rb', 'wc -l')
24

Coincidencia de Patrones (3.0+)

Coincidencia de Patrones Básica

La coincidencia de patrones (palabra clave in) deconstruye arrays y hashes, vinculando variables. => vincula valores coincidentes a variables. *rest captura elementos restantes del array. La coincidencia de hash es parcial: las claves extra se ignoran. | coincide múltiples patrones. case/in es la forma primaria; la forma de una línea es expresión in patrón. La coincidencia de patrones es especialmente potente para analizar datos estructurados (JSON, ASTs) y reemplazar cadenas if-else complejas.

ruby
# in: pattern matching (Ruby 2.7+, stable in 3.0)
case [1, 2, 3]
in [1, *rest]
  puts "Starts with 1, rest: #{rest}"
end

case {name: 'Alice', age: 30}
in {name: String => name, age: Integer => age}
  puts "#{name} is #{age}"
end

# Multiple patterns with |
case status
in :success | :ok
  puts "Good"
else
  puts "Other"
end

# Array patterns
case [1, 2, 3, 4]
in [_, _, *rest]
  puts "Rest: #{rest}"   # [3, 4]
end

# Hash patterns (partial match by default)
case {a: 1, b: 2, c: 3}
in {a: Integer}
  puts "Has a"   # Matches (ignores b, c)
end

Vinculación de Variables y Guards

=> vincula valores coincidentes a variables. El operador pin (^var) coincide contra el valor actual de la variable (no vinculando). Los guards (if/unless) añaden condiciones. Los patrones de array admiten * para splats en cualquier posición. El patrón Find [*, target, *] busca un elemento dentro de un array. La coincidencia de patrones es declarativa y concisa para extracción de datos compleja. Las variables vinculadas en patrones están disponibles después de la instrucción case.

ruby
user = {name: 'Alice', age: 30, role: :admin}

case user
in {name: String => name, age: Integer => age, role: :admin} if age > 18
  puts "#{name} is an adult admin"
end

# Pin operator (^): match against variable value
expected = 'Alice'
case user
in {name: ^expected}
  puts "Matched expected name"
end

# Array destructuring with binding
case [1, 2, 3]
in [first, *middle, last]
  puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end

# Find pattern (search within array)
case [1, 2, 3, 4, 5]
in [*, 3 => three, *]
  puts "Found 3: #{three}"
end

Patrones de Clase y Tipo

Los patrones de clase coinciden por tipo. Las clases personalizadas admiten deconstrucción implementando deconstruct (patrón array) y deconstruct_keys (patrón hash). El patrón Class[args] usa deconstruct; Class(key:) usa deconstruct_keys. Muchas clases integradas (Time, Date, MatchData) admiten deconstrucción. Esto habilita coincidencia expresiva en objetos de dominio. Implemente estos métodos para hacer sus clases pattern-matcheable.

ruby
# Match by class
case value
in Integer
  puts "Integer"
in String
  puts "String"
in Array
  puts "Array"
end

# Class with destructuring
class Point
  attr_reader :x, :y
  def initialize(x, y) @x, @y = x, y end
  def deconstruct = [@x, @y]              # Array pattern
  def deconstruct_keys(keys) = {x: @x, y: @y}  # Hash pattern
end

p = Point.new(1, 2)
case p
in Point[x, y]
  puts "Point at (#{x}, #{y})"
end

case p
in Point(x:, y:)
  puts "X: #{x}, Y: #{y}"
end

# Built-in classes support destructuring
case Time.now
in Time(hour: h) if h < 12
  puts "Morning"
end

Coincidencia de Patrones de Una Línea

La forma de una línea (expr in pattern) devuelve true/false en lugar de lanzar NoMatchingPatternError. Vincula variables en coincidencia exitosa. Útil para cláusulas guard y extracción condicional. La forma hacia la derecha (pattern => var) vincula toda la coincidencia. La coincidencia de una línea es concisa para casos simples; use case/in para lógica compleja de múltiples ramas. Las variables vinculadas en coincidencias fallidas permanecen nil.

ruby
# One-line form (Ruby 3.0+)
{status: :ok, data: 42} in {status: :ok, data: Integer => data}
puts data   # 42

# Returns true/false (does not raise)
[1, 2, 3] in [Integer, Integer, Integer]   # true
[1, 2, 3] in [String, *, *]                # false

# Useful for conditionals
if user in {role: :admin}
  puts "Admin access"
end

# Guard clauses
def process(data)
  return unless data in {type: String, value: Integer}
  # ...
end

# Extracting from JSON
json = JSON.parse('{"user":{"name":"Alice","age":30}}')
json in {"user" => {"name" => String => name, "age" => Integer => age}}
puts "#{name}, #{age}"   # Alice, 30

Casos de Uso Prácticos

La coincidencia de patrones destaca en: análisis de datos estructurados (JSON, XML), máquinas de estados, traversal de AST y despacho por forma de datos. Reemplaza cadenas if-else verbosas con patrones declarativos. La cláusula else maneja formatos inesperados. Combine con guards para condiciones complejas. La coincidencia de patrones hace el código más legible y mantenible para manejo de datos complejo. Es una de las características más potentes de Ruby 3 para código limpio y expresivo.

ruby
# 1. Parse JSON API responses
def handle_response(response)
  case JSON.parse(response)
  in {"status" => "success", "data" => Array => items}
    items.each { |item| process(item) }
  in {"status" => "error", "message" => String => msg}
    raise "API error: #{msg}"
  in {"status" => "error", "code" => Integer => code} if code >= 500
    retry_request
  else
    raise "Unknown response format"
  end
end

# 2. State machines
case state
in :idle, event: :start
  transition_to :running
in :running, event: :pause
  transition_to :paused
in :paused, event: :resume
  transition_to :running
in :running | :paused, event: :stop
  transition_to :idle
end

# 3. AST traversal
def evaluate(node)
  case node
  in {type: 'number', value: n}
    n
  in {type: 'add', left:, right:}
    evaluate(left) + evaluate(right)
  in {type: 'mul', left:, right:}
    evaluate(left) * evaluate(right)
  end
end
25

Metaprogramación

send y define_method

define_method crea métodos dinámicamente. send llama métodos por nombre (incluso privados). public_send respeta la visibilidad. Útil para DSLs y reducir código repetitivo. Tenga cuidado con la entrada del usuario para evitar problemas de seguridad. La metaprogramación es potente pero puede hacer el código más difícil de entender.

ruby
class Foo
  define_method(:greet) do |name|
    "Hello, #{name}"
  end
end
Foo.new.greet("Alice")  # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob")  # "Hello, Bob"
Foo.new.public_send(:greet, "Bob")  # Respects visibility

method_missing

method_missing intercepta llamadas a métodos no definidos. Útil para despacho dinámico y DSLs. Siempre sobrescriba respond_to_missing? para coincidir. Puede ser lento y ocultar bugs. Prefiera define_method cuando el conjunto de métodos es conocido. ActiveRecord lo usa para accessores de atributos.

ruby
class DynamicHash
  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      self[name.to_s.chomp('=')] = args.first
    else
      self[name.to_s]
    end
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end

eval

eval ejecuta una cadena como código Ruby. Extremadamente potente pero peligroso. Nunca evalúe entrada no confiable (inyección de código). Use binding.eval para un contexto específico. Para evaluación segura, use un parser como Ripper o un sandbox. La mayoría de los casos de uso tienen alternativas más seguras.

ruby
# Execute a string as Ruby code
result = eval("1 + 2 * 3")  # 7
# Dynamic method definition
eval("def dynamic_method; 42; end")
dynamic_method  # 42
# WARNING: never eval untrusted input!

Clases Abiertas

Las clases de Ruby son abiertas: puede añadir métodos a cualquier clase, incluyendo las integradas. Esto se llama monkey patching. Potente para arreglos rápidos pero puede causar conflictos y confusión. Use refinements para modificaciones con ámbito. Documente los cambios claramente. Prefiera composición sobre monkey patching.

ruby
class String
  def shout
    upcase + "!"
  end
end
"hello".shout  # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflicts

Macros de Clase

Las macros de clase son métodos de clase que definen otros métodos. has_many, belongs_to, attr_accessor son ejemplos. Usan define_method internamente. Así es como Rails crea métodos dinámicos. DSLs como RSpec y Sinatra usan este patrón. Hace el código declarativo y legible.

ruby
class ActiveRecord::Base
  def self.has_many(name)
    define_method(name) { [] }
  end
end
class Post < ActiveRecord::Base
  has_many :comments
end
Post.new.comments  # []

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.