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.
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) # trueSí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.
status = :active
puts status.class # Symbol
puts status.to_s # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status] # activeNil 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.
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" # falsyConversió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.
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.
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} hereCadenas
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.
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, W0rldMutació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.
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.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentationFormato (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.
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 floatConcatenació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.
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, s4Estructuras 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.
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) # trueHashes
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.
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.
(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.
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.
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)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.
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 >= 60Case (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.
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"
endWhile / 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.
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
endIteradores (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.
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 3Break / 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.
[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 # 20Mé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 =.
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) # 10Bloques 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.
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 } # 10Procs 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.
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 methodObjeto 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.
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.inspectValores 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.
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 = coordsClases 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.
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.countHerencia 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.
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.
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 walkingControl 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.
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 methodSelf 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.
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 # 2Manejo 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).
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/failureRaise 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.
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}"
endRetry
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.
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 topModificador 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.
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.
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 loopsE/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.
# 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.
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}" }
endExistencia 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.
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.
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 } # recursiveCSV 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.
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"] # AliceFecha/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.
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 # tomorrowAná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.
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/.
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.
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/2024Opciones 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).
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"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.
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 methodsmethod_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.
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.
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.
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 preemptivelySend 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.
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!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).
# 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
endProcs 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.
# 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 # 2Objetos 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.
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, 3Closures 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.
# 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) # 42Iteradores 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.
# 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) }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.
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_aEvaluació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.
# 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.
# 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_aOrdenamiento 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.
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 # 1Iteració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.
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"]}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).
# 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'
endComandos 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.
# 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 cleanGemspec (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.
# 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.gemTareas 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].
# 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'
endRbenv 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.
# 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 -vFundamentos 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.
# 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
endModelos 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.
# 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.
# 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
endVistas 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.
<!-- 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.
# 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, :integerTesting 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/.
# 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) }
endMocks 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).
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.
# 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).
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 evaluationPruebas 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.
# 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
endOperaciones 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.
# 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.
# 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.closeOperaciones 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.
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 \).
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 afterCSV 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.
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"))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 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 reasonConversió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.
# 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_8Mé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.
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.
# 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.stringSí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.
# 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)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.
# 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 # NoMethodErrorInclude 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.
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 saveMixin 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.
# 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 # falseMé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.
# 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) # trueRefinements (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.
# 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 inBloques 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).
# 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.
# 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
endObjetos 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.
# 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.
# 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.
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.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.
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
endClases 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.
# 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.
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, prependedeval 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.
# 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.
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) # trueTesting (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.
# 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: rspecMocks 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).
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
endMinitest
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).
# 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.rbDatos 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.
# 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.
# 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
endThreads 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.
# 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 threadSincronizació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.
# 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.newFibers
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).
# 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 34Ruby 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.
# 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.shutdownRactors (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.
# 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 safelyCodificació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 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.
# 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.
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
endE/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.
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.
# 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')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.
# 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)
endVinculació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.
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}"
endPatrones 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.
# 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"
endCoincidencia 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.
# 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, 30Casos 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.
# 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
endMetaprogramació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.
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 visibilitymethod_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.
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
endeval
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.
# 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.
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflictsMacros 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.
class ActiveRecord::Base
def self.has_many(name)
define_method(name) { [] }
end
end
class Post < ActiveRecord::Base
has_many :comments
end
Post.new.comments # []Fragmentos de Ruby relacionados
Copy-paste ready code for common tasks.
Blocks, Procs, Lambdas
Use blocks with yield, Procs, lambdas, and the & operator.
Classes and Modules
Define classes with inheritance, mix in modules, and add class methods.
Iterators
Use each, map, select, reduce, group_by, and lazy enumerators.
Strings
Interpolate, trim, split, replace, and pattern-match strings.
Hashes
Build, default, transform, merge, and group with Hash.
Metaprogramming
Define methods dynamically, intercept with method_missing, and build DSLs.
Error Handling
Raise and rescue typed exceptions with ensure and retry.
File I/O
Read, write, append, traverse directories, and process CSV files.
Was this helpful?