Skip to content

Ruby Cheatsheet

Dynamic, elegant language optimized for developer happiness.

01

Basics

Variables & Types

Ruby is dynamically typed—variables don't need type declarations. Use .class to inspect the type and is_a? for type checks. Everything is an object, including numbers and booleans.

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

Symbols

Symbols (:name) are immutable, interned strings—only one copy exists in memory. Use them for hash keys, method names, and identifiers where identity matters more than content. More memory-efficient than strings for repeated use.

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

Nil & Truthiness

In Ruby, only nil and false are falsy—everything else (including 0, '', and []) is truthy. This differs from many languages where 0 is falsy. Use || for defaults and nil? to check for nil specifically.

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

Type Conversion

to_i/to_s/to_f are lenient conversions (return 0 on failure). Integer()/Float() are strict (raise ArgumentError). Use strict conversion when you need validation, lenient when you want graceful degradation.

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

String Interpolation

Double-quoted strings support #{expr} interpolation—any Ruby expression inside. Single-quoted strings are literal (no interpolation or escapes except \\ and \'). Use double quotes when you need interpolation or escape sequences.

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

Strings

Common String Methods

Ruby strings have rich methods. Most return new strings (strings are mutable in Ruby). Use ! variants (upcase!, gsub!) for in-place modification—these modify the receiver and return nil if no change.

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

String Mutation (Bang Methods)

Methods ending with ! modify the object in-place and are often more efficient. They may return nil if no change was made. Use when you want to avoid creating copies, but be aware of side effects.

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

Heredoc & Multiline

Heredocs (<<TEXT ... TEXT) create multiline strings. <<~ (squiggly heredoc) strips common leading whitespace for clean code. Useful for SQL queries, HTML templates, or long messages.

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

Formatting (sprintf)

Use % operator or format/sprintf for C-style string formatting. %-10s left-aligns in 10 chars, %08.2f zero-pads to 8 chars with 2 decimals. Useful for tabular output and fixed-width formatting.

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

String Concatenation

+ creates a new string, << appends in-place (more efficient for building strings). * repeats a string. join combines array elements with a separator. Prefer << or join over repeated + for performance.

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

Data Structures

Arrays

Arrays are ordered, zero-indexed, and can hold mixed types. << and push add to the end. Use include? for membership, sum for totals. Arrays are mutable—use freeze to make them immutable.

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

Hashes

Hashes are key-value dictionaries. Symbol keys (name:) are idiomatic and efficient. Use key? for existence, fetch for safe access (raises on missing), transform_values for bulk updates.

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

Ranges

'..' includes the end, '...' excludes it. Ranges are lazy and memory-efficient for large sequences. Useful for iteration, slicing, and generating sequences. Can be used with any Comparable type.

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

Enumerable Methods

Enumerable is Ruby's most powerful mixin—map, select, reject, reduce, find, group_by, sort_by, etc. Use &:method as shorthand for { |x| x.method }. These enable expressive data transformation pipelines.

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

Sets

Set (from 'set' library) stores unique elements with O(1) lookup. Use for deduplication and set operations (union, intersection, difference). Convert with to_a when you need an array. Requires 'set' require.

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

Control Flow

If / Elsif / Unless

if/elsif/else is standard branching. unless is the opposite of if (executes when condition is false). Modifier form (statement if condition) is idiomatic for single-line guards—improves readability for simple cases.

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

Case (When)

case/when uses === for matching, enabling ranges, classes, and regex. Multiple values separated by commas. then allows single-line bodies. Without a target, case acts as a cleaner if/elsif chain.

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

While / Until / Loop

while runs while true, until runs until true (while false). loop is infinite—use break to exit. Prefer iterators (each, map) over while for collections—they're more idiomatic and less error-prone.

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

Iterators (Each/Times/Upto)

Ruby's iterators are more idiomatic than for loops. times for counting, upto/downto for ranges, each_with_index for index+value. step controls increment. These are the backbone of Ruby iteration.

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

Break / Next / Redo

next skips to the next iteration (like continue), break exits the loop early. break can return a value from a block. redo restarts the current iteration without re-checking the condition—rarely used.

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

Methods & Blocks

Method Definition

Methods use def/end. Keyword arguments (key:) improve readability for many params. *args collects extra positional args into an array, **kwargs collects keyword args into a hash. Default values use =.

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

Blocks & Yield

Blocks are anonymous chunks of code passed to methods. yield invokes the block. Blocks are everywhere in Ruby (each, map, etc.). Use yield to make your methods flexible—callers provide the behavior.

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

Procs & Lambdas

Procs and lambdas are reusable blocks stored in variables. Key difference: lambdas check argument count (raise on mismatch) and return only from themselves; procs are lenient and return from the enclosing method. Prefer lambda for strict behavior.

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

Method Object (&)

&:method converts a method name to a Proc. It's the idiomatic shorthand for simple one-method blocks: map(&:to_i) instead of map { |s| s.to_i }. Cleaner and more readable for simple transformations.

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

Return Values

Ruby methods implicitly return the last evaluated expression—no need for explicit return. Use return for early exits. Multiple values are returned as an array and can be destructured. This makes code concise.

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

Classes & OOP

Class & Instance Variables

@var = instance variable (per object), @@var = class variable (shared). attr_accessor generates getter+setter, attr_reader getter only, attr_writer setter only. self.method defines class methods.

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

Inheritance & Super

< denotes inheritance (single inheritance only). super calls the parent's version of the current method. Use super (with parens to pass args, without to pass same args) to extend parent behavior. Ruby uses single inheritance + mixins.

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

Modules (Mixins)

Modules group reusable methods. include adds instance methods, extend adds class methods. This is Ruby's solution to multiple inheritance. Enumerable is a famous mixin—include it and define each to get map, select, etc.

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

Access Control

public (default), private (only callable without explicit receiver), protected (callable within the class hierarchy). Use private for internal helpers, protected for methods shared between instances of the same class.

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

Self & Class Methods

self refers to the current object. Inside class body, self is the class—def self.method defines class methods. Class methods are called on the class (Counter.total), instance methods on instances. alias_method creates a method alias.

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

Error Handling

Begin / Rescue / Ensure

begin/rescue is Ruby's try/catch. Rescue specific exception classes for targeted handling. => e captures the exception object. ensure runs always—use for cleanup (closing files, releasing locks).

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

Raise & Custom Exceptions

raise throws an exception (raise without args re-raises). Custom exceptions inherit from StandardError (or a more specific class). Name them with an Error suffix. Rescue by class to handle specific failure modes.

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

Retry

retry restarts the begin block from the beginning. Use for transient failures (network, rate limits) with a counter to avoid infinite loops. Without a limit, retry can hang your program—always guard it.

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

Rescue Modifier

rescue as a modifier is a concise way to provide a fallback value. It catches StandardError and returns the right side. Use for simple cases—avoid for complex logic as it hides errors. Great for parsing or optional operations.

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

Throw / Catch (Control Flow)

throw/catch is NOT exception handling—it's a control flow mechanism for early exit from deep nesting (unlike other languages). throw :symbol jumps to the matching catch. Use for breaking out of nested loops; use begin/rescue for actual errors.

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

File I/O

Read & Write Files

File.write/File.read are simple one-shot methods. File.open with a block auto-closes the file. File.foreach reads line by line without loading the whole file—memory-efficient for large files. chomp removes trailing newline.

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

File Block (Auto-Close)

Always use File.open with a block—it guarantees the file is closed even if an error occurs. The block form is the idiomatic, safe way to handle files. Without a block, you must manually call close.

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

File Existence & Info

File class provides filesystem queries. file?/directory? distinguish types. mtime/ctime/atime give timestamps. rename/delete modify the filesystem. Always check existence before operations to avoid errors.

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

Directory Operations

Dir manages directories. mkdir creates, chdir changes (block form restores after). glob matches file patterns—* matches any, ** matches recursively. Useful for file discovery and batch processing.

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

CSV & JSON

CSV and JSON are in the standard library. CSV.foreach streams rows (memory-efficient). JSON.parse returns hashes/arrays with string keys. to_json serializes any object. These are essential for data interchange.

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

Date/Time & Regex

Time & Date

Time represents a moment (with timezone). Date represents a calendar date (no time). Arithmetic on Time uses seconds. strftime formats with %Y (year), %m (month), %d (day), %H:%M (time). Require 'time' for ISO8601 parsing.

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

Date Parsing & Arithmetic

Date arithmetic works naturally—adding integers adds days. next_month/prev_month handle month boundaries. Date - Date returns a Rational (days). upto/downto iterate over date ranges. Use Date for calendar logic, Time for timestamps.

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

Regex Matching

=~ returns the match position or nil. $1, $2 hold captured groups after a match. .match returns a MatchData object for more detail. Use =~ for simple checks, .match for extracting captures. Regex literals use /pattern/.

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

Regex Substitution

gsub replaces all matches (sub replaces first). Pass a block for dynamic replacement. scan extracts all matches into an array. Backreferences (\1, \2) in replacement strings refer to captured groups. Powerful for text processing.

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

Regex Options

Regex options: i (case-insensitive), m (multiline—dot matches newline), x (extended—allows whitespace/comments in pattern). %r{} is an alternate delimiter useful when the pattern contains slashes (like URLs).

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

Metaprogramming & Concurrency

Dynamic Method Definition

define_method creates methods at runtime. Use to generate multiple similar methods or build DSLs. This is metaprogramming—code that writes code. Powerful but use judiciously; it can make code harder to understand.

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

method_missing

method_missing intercepts calls to undefined methods. Use to build flexible APIs or proxies. Always override respond_to_missing? too, so reflection works. Use sparingly—it can hide bugs and confuse static analysis.

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

Threads

Threads run code concurrently. join waits for completion. MRI (standard Ruby) has a GIL, so CPU-bound threads don't run in true parallel—I/O-bound threads do. For true parallelism, use Ractor (Ruby 3.0+) or multiple processes.

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

Fibers (Cooperative)

Fibers are lightweight, cooperative concurrency— they pause and resume manually via yield/resume. Unlike threads, they don't run in parallel. Use for generators, lazy evaluation, or pausable computations. Lower overhead than threads.

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

Send & Eval

send calls any method by name (including private). public_send respects visibility. eval executes a string as Ruby code—extremely powerful but dangerous with untrusted input (code injection). Use send for dynamic dispatch, avoid eval in production.

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

Blocks, Procs & Lambdas

Blocks Basics

Blocks are Ruby's most common closure—anonymous code passed to methods via { } or do...end. yield invokes the block. |n| declares block parameters. block_given? checks if a block was passed. Blocks are not objects (can't be stored in variables)—use Proc/Lambda for that. Every method can accept an implicit block, making DSLs natural (Rails uses this heavily).

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

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

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

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

Procs vs Lambdas

Procs and Lambdas are both callable objects (blocks turned into objects). Procs are lenient: extra args become nil, missing args are nil, and 'return' exits the enclosing method. Lambdas are strict: they check argument count and 'return' only exits the lambda. Use lambdas when you want method-like behavior; Procs when you want block-like behavior. -> (stabby lambda) is the modern concise syntax.

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

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

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

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

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

Method Objects

method(:name) retrieves a method as a Method object (callable like a Proc). &:symbol converts a symbol to a proc that sends that method—extremely common idiom: array.map(&:to_s). The & prefix converts a Proc to a block (or vice versa). This enables passing methods as arguments elegantly. Method objects retain their receiver, so they're bound to the object.

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

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

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

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

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

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

Closures and Binding

Closures (blocks, procs, lambdas) capture variables by reference—they see updates to captured variables. This enables stateful closures (counters, accumulators). Proc#binding gives access to the closure's environment (for advanced metaprogramming). The Kernel#binding method captures the current execution context for eval. Closures are why Ruby blocks are so powerful for callbacks and iterators.

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

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

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

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

Custom Iterators with Blocks

Including Enumerable and defining #each gives you map, select, reduce, and 50+ methods for free—this is Ruby's iterator protocol. each_with_object is cleaner than inject for building accumulators. tap inserts side effects into method chains (great for debugging). Custom methods with yield let you create your own DSLs (with_timing, with_database, etc.). Blocks make Ruby's iteration and callback patterns elegant.

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

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

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

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

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

Enumerable & Iterators

Core Enumerable Methods

Enumerable is Ruby's most powerful mixin—include it and define #each to get 50+ methods. map transforms, select/reject filter, reduce/inject aggregates, find returns first match, group_by/partition cluster. Symbol shortcuts (reduce(:+)) are idiomatic. These methods work on Arrays, Hashes, Ranges, Files—anything with #each. Mastering Enumerable is key to idiomatic Ruby.

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

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

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

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

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

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

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

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

Lazy Evaluation

lazy converts an enumerable to a lazy one—values are computed only when needed. This enables infinite sequences and avoids computing the entire collection for chains that only need a few results. Without lazy, map/select create intermediate arrays. With lazy, the pipeline pulls values one at a time. Use lazy for large/infinite datasets or expensive transformations. The trade-off: lazy has per-element overhead, so it's slower for small collections.

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

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

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

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

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

Enumerator and External Iteration

Enumerator wraps an iteration as an object—you can call .next manually (external iteration) instead of using a block (internal iteration). This enables pausing/resuming iteration, peeking, and creating infinite sequences. Enumerator.new with a block lets you build custom iterators (generators). Most Enumerable methods return Enumerators when called without a block: [1,2,3].map returns an Enumerator you can chain.

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

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

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

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

Sorting and Comparing

sort takes a block comparing two elements (return -1, 0, or 1). sort_by is more efficient—it computes the sort key once per element (Schwartzian transform) rather than on every comparison. Use sort_by for complex keys. The <=> (spaceship) operator returns -1/0/1 and is the basis of Ruby's sorting. min/max/min_by/max_by find extremes. Include Comparable and define <=> for natural ordering of your objects.

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

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

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

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

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

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

Hash Iteration and Transformation

Hashes are Enumerable too. each/each_pair iterate key-value pairs. transform_keys/transform_values (Ruby 2.5+) create new hashes with modified keys/values. select/reject filter into a new hash. merge combines hashes (block resolves conflicts). group_by builds a hash from an array. Hash iteration is the foundation of data processing in Ruby—master these for clean, expressive data manipulation.

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

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

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

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

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

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

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

Gems & Bundler

Gem Basics

Gems are Ruby packages (libraries). gem install manages system gems. For projects, use Bundler with a Gemfile to pin versions and manage dependencies. Version specifiers: '~> 1.4' (pessimistic, allows patches), '>= 5.0' (optimistic). Groups (:development, :test, :production) let you load only needed gems per environment. require: false means Bundler won't auto-require it (you require manually when needed).

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

# List installed gems
$ gem list
$ gem list rails

# Uninstall
$ gem uninstall rails

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

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

group :production do
  gem 'newrelic_rpm'
end

Bundler Commands

Bundler ensures your project uses the exact gem versions specified. bundle install reads Gemfile and writes Gemfile.lock (exact versions for reproducibility—commit this!). bundle exec runs commands with the correct gem versions (avoids conflicts). bundle update changes versions (be careful—can break things). Always use bundle exec for rake/rspec/rails to ensure the right gems load. Gemfile.lock makes deployments reproducible.

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

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

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

# Check for dependency issues
$ bundle check
$ bundle doctor

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

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

# Clean old gems
$ bundle clean

Gemspec (Creating a Gem)

The .gemspec file defines a gem's metadata and dependencies. spec.files lists included files; require_paths tells Ruby where to find them. add_dependency for runtime deps, add_development_dependency for test/build deps. required_ruby_version enforces Ruby version. gem build creates the .gem package; gem install installs it locally. Publish to rubygems.org with gem push. Structure: lib/ for code, spec/ for tests.

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

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

  spec.required_ruby_version = ">= 3.0"

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

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

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

Rake Tasks

Rake is Ruby's make—a task runner. Define tasks with task :name do ... end. Namespaces group related tasks. File tasks have dependencies (rebuild if source changes). sh runs shell commands. Rake is used for tests, builds, deployments, and database tasks (Rails uses it heavily). Run with rake task_name. The default task runs when you type just rake. Pass arguments with rake greet[Alice].

ruby
# Rakefile
require 'rspec/core/rake_task'

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

task default: :spec

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

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

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

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

Rbenv and RVM (Ruby Versions)

rbenv and RVM manage multiple Ruby versions on one machine. rbenv is lightweight (shims); RVM is heavier (overrides shell commands). .ruby-version file (committed) ensures everyone uses the same Ruby version. Gemsets (RVM) or bundle config path isolate project gems. For production, use bundle config set path to install gems locally to the project, avoiding system gem pollution. Always pin Ruby version in .ruby-version.

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

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

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

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

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

Rails Basics

MVC Structure

Rails is a MVC framework: Models (ActiveRecord) handle data, Controllers (ActionController) handle HTTP requests, Views (ActionView) render responses. resources generates 7 RESTful routes automatically. Routes map URLs to controller actions. Convention over configuration: name your model User, controller UsersController, and Rails wires everything together. This structure is the backbone of every Rails app.

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

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

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

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

ActiveRecord Models

ActiveRecord is Rails' ORM—models map to database tables. validates enforces data integrity. Associations (has_many, belongs_to, has_one) define relationships. Callbacks (before_save, after_create) hook into the lifecycle. Scopes are reusable query fragments. ActiveRecord uses convention: User model → users table, created_at/updated_at columns. It's the heart of Rails—master it for effective Rails development.

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

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

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

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

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

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

Controllers and Strong Params

Controllers handle HTTP requests and coordinate models/views. before_action runs filters (authentication, loading resources). Strong parameters (permit) prevent mass-assignment vulnerabilities—only whitelisted fields can be set. redirect_to sends the user elsewhere; render shows a view. The @instance variables are available in views. RESTful actions (index, show, new, create, edit, update, destroy) are conventional.

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

  def index
    @users = User.all
  end

  def show; end  # @user set by before_action

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

  private

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

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

Views and Helpers

ERB (Embedded Ruby) is Rails' default templating: <%= %> outputs, <% %> executes. link_to/button_to generate HTML links/forms. form_with builds forms tied to models. Partials (_form.html.erb) are reusable view fragments rendered with render. Path helpers (new_user_path, user_path(user)) generate URLs from routes. Helpers keep views clean. For complex logic, use view helpers (app/helpers/) or decorators.

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

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

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

Migrations and Database

Migrations evolve the database schema over time, version-controlled. create_table defines tables; add_column/remove_column modify them. t.references creates foreign keys. t.timestamps adds created_at/updated_at. db:migrate applies pending migrations; db:rollback undoes the last. The schema.rb file is the authoritative source of the current schema (auto-generated). Never edit schema.rb directly—use migrations. This makes database changes reproducible across environments.

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

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

      t.timestamps  # created_at, updated_at
    end
  end
end

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

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

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

RSpec Testing

Basic Syntax (describe, it, expect)

RSpec is Ruby's dominant testing framework. describe groups related tests; it defines a single test. expect(...).to / not_to make assertions. let defines lazy memoized variables (computed once when first accessed). context is an alias for describe, used for branches (when...). The shoulda-matchers gem provides one-liner syntax for common Rails validations/associations. Tests go in spec/ mirroring app/ structure.

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

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

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

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

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

Mocks and Stubs

Stubs (allow) replace method return values; mocks (expect) verify a method was called. Doubles are fake objects for testing (faster than real objects). Use stubs to isolate the code under test from external dependencies (APIs, databases). Use mocks to verify interactions. Over-mocking makes tests brittle—prefer real objects when fast enough. FactoryBot creates test data; use create (saves to DB) or build (in-memory only).

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

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

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

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

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

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

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

FactoryBot and Fixtures

FactoryBot creates test objects with sensible defaults. Traits create variations (:admin, :inactive). Sequences generate unique values (emails). create persists to the database; build doesn't. create_list makes multiple. Override any attribute by passing it. Factories are more flexible than fixtures (YAML) but slower (DB writes). Use build_stubbed for fast tests that don't hit the DB. Keep factories simple—complex factories indicate complex models.

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

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

    trait :inactive do
      active { false }
    end

    # Associations
    company { association(:company) }

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

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

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

Before/After Hooks

before hooks run setup code: before(:each) (most common) before every test, before(:all) once per group. after hooks clean up. DatabaseCleaner manages test database state (transaction for speed, truncation for thoroughness). Prefer let over before(:each)—let is lazy (only computes when used) and memoized, while before runs even if the test doesn't need it. Use before for side effects that must happen (logging, time freezing).

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

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

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

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

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

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

Integration and System Tests

Request specs test the full stack (routing → controller → model → view) via HTTP. System specs (Capybara) drive a real browser—filling forms, clicking, checking page content. Unit tests (model specs) are fast and isolated; integration/system tests are slower but catch wiring bugs. Use the testing pyramid: many fast unit tests, fewer integration tests, minimal system tests. have_http_status checks response codes; visit/fill_in/click_button drive the browser.

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

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

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

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

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

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

File & Directory Operations

Reading Files

File.read loads the entire file into memory—fine for small files. File.foreach reads line by line—essential for large files (won't blow memory). File.open with a block auto-closes the file (RAII). readlines returns an array of lines (with newlines—use chomp). binread for binary files. Always check File.exist? before reading if the file might not exist, or rescue Errno::ENOENT.

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

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

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

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

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

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

Writing Files

File.write is the simplest way to write a file (overwrites by default). Use mode: 'a' to append. File.open with a block ensures the file is closed even if an exception occurs. puts adds a newline; write doesn't. << is an alias for write (common in Ruby). For logs, open once and write multiple times (buffered for performance). Always close files or use the block form to avoid resource leaks.

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

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

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

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

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

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

Directory Operations

Dir.glob with patterns finds files (** for recursive). FileUtils provides robust file operations: mkdir_p creates nested directories, cp_r copies recursively, rm_rf removes forcefully (be careful!). Dir.chdir changes the working directory (use the block form to change temporarily). Dir.entries includes . and ..; glob doesn't. Prefer FileUtils over manual File operations for cross-platform safety.

ruby
require 'fileutils'

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

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

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

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

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

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

Pathname and Tempfile

Pathname is an object-oriented wrapper for file paths—cleaner than string manipulation. It provides dirname, basename, extname, join, and file checks as methods. Tempfile creates temporary files that are automatically deleted (use the block form). Dir.mktmpdir creates temporary directories. Use these for clean, safe path handling and temporary file management. Pathname composes paths safely across platforms (handles / vs \).

ruby
require 'pathname'
require 'tempfile'

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

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

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

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

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

CSV and JSON

CSV and JSON are built into Ruby's standard library. CSV.foreach reads row by row (memory efficient); CSV.read loads everything. headers: true treats the first row as column names. JSON.parse converts JSON to Ruby hashes/arrays; to_json serializes Ruby objects. symbolize_names gives symbol keys (cleaner). For YAML, use require 'yaml' and YAML.load_file. These are essential for data interchange in Ruby scripts and web apps.

ruby
require 'csv'
require 'json'

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

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

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

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

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

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

Encoding & String Internals

String Encodings

Ruby strings carry their encoding (usually UTF-8). force_encoding reinterprets bytes as a different encoding (no conversion—use when you know the bytes are already in that encoding). encode actually converts between encodings. valid_encoding? checks if the bytes are valid for the string's encoding. Encoding issues cause the dreaded Encoding::CompatibilityError. Always know your data's encoding; default to UTF-8.

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

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

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

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

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

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

Encoding Conversion and I/O

File I/O uses Encoding.default_external for reading. Specify encoding per-file with the encoding: option. The 'source:target' syntax (ISO-8859-1:UTF-8) reads in the source encoding and converts to the target. Setting default_internal makes Ruby auto-convert all read strings to that encoding. For web apps, everything should be UTF-8. When processing legacy data, explicitly specify encodings to avoid corruption.

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

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

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

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

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

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

String Methods Deep Dive

Ruby strings have rich methods. Inspection (length, include?, start_with?) checks properties. Transformation methods return new strings (strings are mutable in Ruby, but these don't mutate). Substring access uses [start, length] or ranges. sub replaces first match; gsub replaces all (supports regex and blocks). split/join convert between strings and arrays. Note: Ruby 3.0+ frozen_string_literal pragma makes strings immutable for performance—use << or + for building.

ruby
s = "Hello, World"

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

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

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

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

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

Frozen Strings and Performance

frozen_string_literal: true (magic comment at file top) makes all string literals immutable—this is a performance optimization (frozen strings can share memory) and prevents accidental mutation bugs. For building strings, use << (in-place append, O(n)) not += (creates new string each time, O(n²)). join is cleanest for arrays. StringIO acts like a file but writes to a string—useful for building complex output. Ruby 3.x encourages frozen strings by default.

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

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

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

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

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

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

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

Symbols vs Strings

Symbols (:name) are immutable, singleton identifiers—only one :foo exists in memory, ever. Strings are mutable text data with multiple instances. Use symbols for hash keys (faster equality checks), method names, and enum-like values. Use strings for actual text. Symbols are slightly faster for hash keys and comparison. In modern Ruby (2.2+), symbols can be garbage collected, so the old 'symbol memory leak' concern is gone. Rails uses symbols extensively for keys and status values.

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

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

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

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

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

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

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

Modules & Mixins

Module Basics

Modules serve two purposes: namespacing (grouping related code, preventing name clashes) and mixins (sharing behavior without inheritance). Module methods (def self.method) are called on the module. Instance methods (def method) are for mixing into classes. Modules can't be instantiated. Use modules to namespace classes (MyApp::User) and to organize constants and utility functions. This is Ruby's alternative to multiple inheritance.

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

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

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

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

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

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

Include vs Extend vs Prepend

Three ways to mix in modules: include (adds instance methods, goes below the class in lookup), extend (adds class methods), prepend (adds instance methods, goes above the class—can wrap/override). prepend is powerful for before/after hooks (call super to invoke the original). Method lookup: prepend → class → include → superclass. Use include for normal mixins, prepend when you need to wrap existing methods, extend for class-level functionality.

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

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

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

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

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

Enumerable Mixin

Including Enumerable and defining #each gives you map, select, reduce, sort, min, max, and 40+ more methods—this is Ruby's iterator protocol. Including Comparable and defining <=> gives you <, >, ==, between?, clamp, and sort support. These mixins are why Ruby collections are so powerful. Any class that represents a collection or has a natural ordering should include these. It's composition over inheritance.

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

  def initialize(songs)
    @songs = songs
  end

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

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

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

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

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

Singleton Methods and Class Methods

Singleton methods belong to one specific object. Class methods are just singleton methods on the class object. 'class << self' opens the singleton class (eigenclass) to define multiple class methods cleanly. The singleton pattern uses a class variable to hold one instance. Understanding singleton classes is key to Ruby's object model—every object has a singleton class holding its unique methods. This enables per-object customization and metaprogramming.

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

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

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

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

Refinements (Scoped Monkey Patching)

Refinements (Ruby 2.1+) allow scoped monkey patching—add methods to existing classes but only where you explicitly 'using' the refinement. This is safer than global monkey patching (which can break other code). Refinements are activated per-scope (file, class, method). They're useful for adding convenience methods without polluting the global namespace. Less common than they should be due to performance and some scoping quirks, but they're the 'right' way to extend core classes.

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

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

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

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

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

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

Blocks Procs Lambdas Deep

Block Basics

Blocks are anonymous closures passed to methods using { } or do...end. yield invokes the block from within the method. Blocks can take parameters via |var|. A method can check if a block was passed with block_given?. Blocks are the foundation of Ruby's iterator pattern and DSLs. They capture variables from the enclosing scope (closures).

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

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

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

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

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

Procs & Lambdas

Procs and lambdas are objects that wrap blocks, allowing storage in variables and passing around. Procs have lenient argument checking (extra args ignored) and return from the enclosing method. Lambdas have strict argument checking and return only from themselves. Use Procs for flexibility (like methods that accept blocks), lambdas for anonymous functions with predictable behavior. The ->() {} syntax (stabby lambda) is concise for one-liners.

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

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

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

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

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

Method Objects

The method() method returns a Method object wrapping an existing method. The & operator converts a Method or Proc to a block (and vice versa). Symbol#to_proc converts :upcase to { |x| x.upcase }, enabling the concise &:symbol syntax. This is idiomatic Ruby for short block operations. Method objects are bound to their receiver, so they remember self when passed around.

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

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

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

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

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

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

Closures & Variables

Closures capture variables by reference, not value. Multiple closures can share state (the counter example). Block-local variables (declared after ; in parameters) shadow outer variables without modifying them. This enables functional patterns like accumulators, generators, and memoization. Be careful: closures holding references can cause memory leaks if not released. Use this pattern for private state encapsulation.

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

increment.call
increment.call
puts counter   # 2

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

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

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

Custom Iterators

Including Enumerable and implementing each gives your class all iterator methods (map, select, reduce, sort, etc.) for free. Pass the block with &block and call it, or use yield. The & operator converts a block to a Proc and back. This is the idiomatic way to make custom collections iterable. Implement each for forward iteration; add reverse_each for bidirectional.

ruby
class Tree
  include Enumerable

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

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

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

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

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

Metaprogramming

Dynamic Methods

method_missing intercepts calls to undefined methods, enabling dynamic dispatch. Always override respond_to_missing? to match. define_method creates methods at runtime, useful for generating similar methods (like ActiveRecord's find_by_*). instance_variable_get/set access instance variables by name. Use metaprogramming sparingly—it makes code harder to understand and debug. Prefer explicit definitions when possible.

ruby
class Person
  attr_accessor :name, :age

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

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

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

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

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

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

Open Classes & Monkey Patching

Ruby classes are open: you can add methods to any class, including built-ins like String. This is powerful but dangerous (monkey patching can break other code). Refinements (Ruby 2.1+) provide scoped monkey patches: they apply only within files/classes that use the module. Prefer refinements over global monkey patches for safer metaprogramming. Document patches clearly and avoid changing core behavior.

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

  def word_count
    split.size
  end
end

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

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

class MyClass
  using ShoutRefinement

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

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

Hooks & Callbacks

Ruby provides lifecycle hooks: inherited (subclass created), included (module included), prepended (module prepended), method_added (method defined), method_removed, method_undefined. These enable frameworks to react to class changes automatically. ActiveRecord uses these to track attributes, Rails uses them for routing. Override hooks as class methods (self.inherited) for class-level events, instance methods for method events.

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

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

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

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

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

# Other hooks: method_undefined, extended, prepended

eval & Binding

eval executes a string as Ruby code, dangerous if input is untrusted (code injection). binding captures the current execution context (variables, self) for later eval. class_eval executes code in a class context (defines methods). instance_eval changes self to the receiver. Use these for DSLs and code generation, but avoid eval on user input. Prefer blocks and define_method for dynamic code. Always sanitize input if eval is unavoidable.

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

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

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

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

puts "racecar".palindrome?   # true

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

# NEVER eval user input (security risk)

Reflection & Introspection

Ruby provides rich reflection: methods lists all methods, instance_variables lists instance variables, ancestors shows the inheritance chain. parameters reveals a method's parameter names and types. respond_to? checks if an object responds to a method. Use reflection for debugging, serialization (inspecting attributes), and building generic tools (like ORMs). Avoid overusing reflection—it bypasses type safety and makes code harder to follow.

ruby
class User
  attr_accessor :name, :email

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

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

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

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

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

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

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

Testing (RSpec/Minitest)

RSpec Basics

RSpec is a BDD-style testing framework. describe groups related tests, context groups by condition, it defines a single test. let creates lazy memoized variables. expect(...).to matcher is the assertion syntax. Common matchers: eq, be_valid, include, raise_error. subject + is_expected reduces boilerplate. Run with rspec (all) or rspec path/to/spec (specific). Use factories (FactoryBot) instead of fixtures for test data.

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

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

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

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

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

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

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

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

RSpec Mocks & Stubs

Stubs (allow) replace method return values; mocks (expect) verify the method was called. double creates a test double (fake object). and_return sets return values, and_raise simulates errors, with sets expected arguments. Use mocks sparingly—over-mocking makes tests brittle. Test behavior, not implementation. Prefer real objects when feasible; use mocks for external services (APIs, email, payment gateways).

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

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

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

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

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

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

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

Minitest

Minitest is Ruby's default testing library (ships with Ruby). It supports both Unit-style (assert/refute) and Spec-style (must_equal) syntax. setup runs before each test. Assertions: assert_equal, assert_nil, assert_raises, assert_includes. Minitest is faster than RSpec and has fewer dependencies. Use Minitest::Mock for mocking, or Mocha gem for more features. Rails uses Minitest by default (ActiveSupport::TestCase).

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

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

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

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

  def test_valid_with_attributes
    assert @user.valid?
  end

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

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

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

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

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

Test Data & Factories

FactoryBot generates test objects with default values, traits for variations, and associations. create persists to DB; build does not. Traits compose (factory :admin_with_posts, traits: [:admin, :with_posts]). Factories are more flexible than fixtures but slower. Fixtures (YAML) are faster but less flexible. Choose based on needs: fixtures for simple data, factories for complex relationships. Avoid factories with too many traits—keep them focused.

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

    trait :admin do
      role { 'admin' }
    end

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

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

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

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

# In tests: users(:alice)

Integration & System Tests

Request specs test the full stack (routing, controllers, models) via HTTP. System specs (Capybara) drive a real browser, testing JavaScript interactions. Use request specs for API endpoints, system specs for user flows. have_http_status checks response codes. visit/fill_in/click_button simulate user actions. System tests are slower but catch integration bugs. Run system tests with js: true for JavaScript-driven pages.

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

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

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

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

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

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

Threads & Fibers

Threads Basics

Threads in Ruby (MRI) are green threads scheduled by the VM—they do not run truly in parallel due to the GIL (Global Interpreter Lock). However, threads are useful for I/O concurrency (network, file operations). join waits for a thread to complete. value retrieves the return value. For CPU-bound parallelism, use multiple processes (fork, Sidekiq) or JRuby (no GIL). Always join threads to avoid them being killed when the main thread exits.

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

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

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

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

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

Thread Synchronization

Mutex.synchronize ensures only one thread executes a block at a time, preventing race conditions. Queue is a thread-safe FIFO—producers push, consumers pop (blocks if empty). ConditionVariable coordinates threads: wait releases the mutex and sleeps, signal/broadcast wakes waiting threads. Always use synchronization for shared mutable state. Deadlocks occur when threads wait on each other—acquire locks in a consistent order.

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

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

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

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

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

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

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

Fibers

Fibers are cooperative lightweight threads: they yield control manually instead of being preempted. resume starts/resumes a fiber; Fiber.yield pauses it and returns a value. Fibers are useful for generators, lazy evaluation, and parsing state machines. Unlike threads, only one fiber runs at a time, so no synchronization is needed. Fibers are cheaper than threads but cannot use multiple cores. Use them for I/O multiplexing (EventMachine, Async).

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

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

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

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

Async & Concurrent Ruby

The async gem provides modern async I/O using Fibers under the hood, enabling high-concurrency network code. concurrent-ruby provides thread-safe abstractions: Future (async result), Promise (chainable), thread pools, and atomic variables. Use async for I/O-bound work (HTTP, databases), thread pools for CPU-bound work. Always shut down pools to avoid hanging. These gems work around MRI's GIL for practical concurrency.

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

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

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

  tasks.each(&:wait)
end

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

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

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

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

Ractors (Ruby 3.0+)

Ractors (Ruby 3.0+) provide true parallelism by avoiding the GIL. Each Ractor has its own heap, so no data races. Communication is via messages (send/receive, take). Objects sent are copied (or moved with Ractor.move) to maintain isolation. Frozen objects can be shared. Ractors are the future of parallel Ruby but have restrictions: most gems are not yet Ractor-safe. Use them for CPU-bound parallel computation where isolation is acceptable.

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

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

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

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

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

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

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

Encoding & IO

String Encoding

Every Ruby string has an encoding (default UTF-8). bytesize is the byte count; length is the character count (different for multibyte encodings). force_encoding changes the encoding tag without converting bytes (use when you know the actual encoding). encode converts between encodings. Always check valid_encoding? before processing external data. Set the source file encoding with the magic comment # encoding: utf-8 (though UTF-8 is default in Ruby 2.0+).

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

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

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

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

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

File IO

File.read loads the entire file into memory; File.foreach reads line by line (memory-efficient for large files). File.write overwrites; mode 'a' appends; 'r+' reads and writes. Binary mode ('rb', 'wb') prevents encoding conversion. Always use the block form (File.open) to ensure the file is closed. FileUtils provides higher-level operations (cp, mv, mkdir_p). Check existence with File.exist? before reading.

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

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

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

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

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

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

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

StringIO & Tempfile

StringIO wraps a string in an IO-like interface, useful for testing file operations without touching disk. Tempfile creates temporary files that are automatically deleted when the block exits (Tempfile.create) or when unlinked (Tempfile.new). Use Tempfile for large data that does not fit in memory or for passing to external programs via path. Always ensure cleanup with ensure blocks. StringIO is great for unit tests of IO code.

ruby
require 'stringio'
require 'tempfile'

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

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

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

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

Network IO

TCPSocket/TCPServer provide low-level TCP access. Net::HTTP is the standard HTTP client (built-in). For complex HTTP needs (sessions, cookies, retries), use the httparty or faraday gems. Always set use_ssl = true for HTTPS. For high-performance HTTP, consider async-http or typhoeus. URI parses URLs safely. Handle Net::ReadTimeout and Errno::ECONNREFUSED for robust network code.

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

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

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

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

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

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

STDIO & Pipelines

$stdout, $stderr, $stdin are global IO objects (use constants STDOUT/STDERR/STDIN for the originals). Redirect by reassigning globals. StringIO captures output for testing. Open3.popen3 gives full control over a subprocess's stdin/stdout/stderr. Open3.pipeline chains commands like a shell pipe. Capture3 returns stdout, stderr, and status. Always close stdin to signal EOF to the subprocess. Use wait.value to get the exit status.

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

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

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

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

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

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

Pattern Matching (3.0+)

Basic Pattern Matching

Pattern matching (in keyword) destructures arrays and hashes, binding variables. => binds matched values to variables. *rest captures remaining array elements. Hash matching is partial: extra keys are ignored. | matches multiple patterns. case/in is the primary form; one-line form is expression in pattern. Pattern matching is especially powerful for parsing structured data (JSON, ASTs) and replacing complex if-else chains.

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

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

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

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

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

Variable Binding & Guards

=> binds matched values to variables. The pin operator (^var) matches against the variable's current value (not binding). Guards (if/unless) add conditions. Array patterns support * for splats at any position. Find pattern [*, target, *] searches for an element within an array. Pattern matching is declarative and concise for complex data extraction. Variables bound in patterns are available after the case statement.

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

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

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

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

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

Class & Type Patterns

Class patterns match by type. Custom classes support destructuring by implementing deconstruct (array pattern) and deconstruct_keys (hash pattern). The pattern Class[args] uses deconstruct; Class(key:) uses deconstruct_keys. Many built-in classes (Time, Date, MatchData) support destructuring. This enables expressive matching on domain objects. Implement these methods to make your classes pattern-matchable.

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

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

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

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

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

One-Line Pattern Matching

The one-line form (expr in pattern) returns true/false instead of raising NoMatchingPatternError. It binds variables on successful match. Useful for guard clauses and conditional extraction. The rightward form (pattern => var) binds the entire match. One-line matching is concise for simple cases; use case/in for complex multi-branch logic. Variables bound in failed matches remain nil.

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

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

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

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

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

Practical Use Cases

Pattern matching excels at: parsing structured data (JSON, XML), state machines, AST traversal, and dispatching on data shape. It replaces verbose if-else chains with declarative patterns. The else clause handles unexpected formats. Combine with guards for complex conditions. Pattern matching makes code more readable and maintainable for complex data handling. It is one of Ruby 3's most powerful features for clean, expressive code.

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

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

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

Metaprogramming

send & define_method

define_method creates methods dynamically. send calls methods by name (even private). public_send respects visibility. Useful for DSLs and reducing boilerplate. Be careful with user input to avoid security issues. Metaprogramming is powerful but can make code harder to understand.

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

method_missing

method_missing intercepts calls to undefined methods. Useful for dynamic dispatch and DSLs. Always override respond_to_missing? to match. Can be slow and hide bugs. Prefer define_method when the set of methods is known. ActiveRecord uses it for attribute accessors.

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

eval

eval executes a string as Ruby code. Extremely powerful but dangerous. Never eval untrusted input (code injection). Use binding.eval for a specific context. For safe evaluation, use a parser like Ripper or a sandbox. Most use cases have safer alternatives.

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

Open Classes

Ruby classes are open: you can add methods to any class, including built-ins. This is called monkey patching. Powerful for quick fixes but can cause conflicts and confusion. Use refinements for scoped modifications. Document changes clearly. Prefer composition over monkey patching.

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

Class Macros

Class macros are class methods that define other methods. has_many, belongs_to, attr_accessor are examples. They use define_method internally. This is how Rails creates dynamic methods. DSLs like RSpec and Sinatra use this pattern. Makes code declarative and readable.

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

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.