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.
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) # trueSymbols
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.
status = :active
puts status.class # Symbol
puts status.to_s # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status] # activeNil & 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.
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" # falsyType 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.
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.
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} hereStrings
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.
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, W0rldString 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.
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.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentationFormatting (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.
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 floatString 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.
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, s4Data 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.
nums = [1, 2, 3, 4, 5]
nums.push(6) # [1,2,3,4,5,6]
nums << 7 # same as push
nums[0] = 0
puts nums.first # 0
puts nums.length # 7
puts nums.sum # 28
puts nums.include?(3) # trueHashes
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.
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.
(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.
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.
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)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.
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60Case (When)
case/when 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.
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
endWhile / Until / Loop
while 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.
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
endIterators (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.
3.times { |i| puts i } # 0 1 2
1.upto(3) { |n| puts n } # 1 2 3
3.downto(1) { |n| puts n } # 3 2 1
[1,2,3].each_with_index do |n, i|
puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n } # 1 2 3Break / Next / Redo
next 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.
[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 # 20Methods & 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 =.
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) # 10Blocks & 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.
def repeat(n)
n.times { yield }
end
repeat(3) { puts "hi" } # prints hi 3 times
def with_result
yield(5)
end
puts with_result { |x| x * 2 } # 10Procs & 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.
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 methodMethod 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.
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.inspectReturn 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.
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 = coordsClasses & 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.
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.countInheritance & 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.
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.
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 walkingAccess 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.
class BankAccount
def initialize(bal); @balance = bal; end
def deposit(amt); @balance += amt; end
def balance; @balance; end
private
def audit; "auditing..."; end
protected
def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance # 100
# a.audit # Error: private methodSelf & 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.
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 # 2Error 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).
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Caught: #{e.message}"
rescue => e
puts "Other error: #{e.class}"
ensure
puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failureRaise & 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.
class InvalidAgeError < StandardError; end
def set_age(age)
raise InvalidAgeError, "Age cannot be negative" if age < 0
raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
@age = age
end
begin
set_age(-5)
rescue InvalidAgeError => e
puts "Custom: #{e.message}"
endRetry
retry 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.
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 topRescue 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.
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.
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 loopsFile 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.
# 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.
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}" }
endFile 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.
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.
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
File.write("a.txt", "a")
puts Dir.pwd # current path
end
Dir.glob("*.txt") { |f| puts f } # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursiveCSV & 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.
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"] # AliceDate/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.
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 # tomorrowDate 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.
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/.
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.
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/2024Regex 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).
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"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.
class Dynamic
[:foo, :bar, :baz].each do |name|
define_method(name) { puts "Called #{name}" }
end
end
d = Dynamic.new
d.foo # Called foo
d.bar # Called bar
# Useful for generating similar methodsmethod_missing
method_missing 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.
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.
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.
fiber = Fiber.new do
Fiber.yield 1
Fiber.yield 2
3
end
puts fiber.resume # 1
puts fiber.resume # 2
puts fiber.resume # 3
# Fibers yield control cooperatively, not preemptivelySend & 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.
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!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).
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n } # do-end for multi-line
[1, 2, 3].each do |n|
puts n * 2
end
# yield: call the block from a method
def greet
puts "before"
yield # invokes the block
puts "after"
end
greet { puts "in block" }
# before / in block / after
# yield with arguments
def compute
result = yield(10, 20)
puts "got: #{result}"
end
compute { |a, b| a + b } # got: 30
# block_given? checks if a block was passed
def maybe_yield
return "no block" unless block_given?
yield
endProcs vs Lambdas
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.
# 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 # 2Method 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.
class Calculator
def add(a, b) a + b end
end
calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3) # 5
m.(2, 3) # 5
m[2, 3] # 5
# Convert method to Proc
p = m.to_proc
p.call(4, 5) # 9
# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase) # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }
# Symbol#to_proc
:upcase.to_proc.call("hello") # "HELLOW"
# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts)) # prints 1, 2, 3Closures 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.
# 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) # 42Custom 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.
# Define your own iterator using yield
class LinkedList
include Enumerable # gets map, select, etc. for free
def each
node = @head
while node
yield node.value
node = node.next
end
end
end
# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}
# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
.tap { |arr| puts "after map: #{arr}" }
.select { |n| n > 2 }
# Custom method with block
def with_timing
start = Time.now
yield
puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }Enumerable & 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.
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_aLazy 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.
# 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.
# 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_aSorting 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.
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 # 1Hash 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.
hash = { a: 1, b: 2, c: 3 }
# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }
# Transform keys/values
hash.transform_keys { |k| k.to_s } # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}
# Filter
hash.select { |k, v| v > 1 } # {b:2, c:3}
hash.reject { |k, v| v > 1 } # {a:1}
# Merge
{ a: 1 }.merge({ b: 2 }) # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new } # {a:3}
# Invert (values become keys)
{ a: 1, b: 2 }.invert # {1=>:a, 2=>:b}
# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}Gems & 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).
# 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'
endBundler 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.
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production # skip group
# Update gems
$ bundle update # update all
$ bundle update rails # update specific gem
$ bundle outdated # show outdated gems
# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate
# Check for dependency issues
$ bundle check
$ bundle doctor
# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!
# Add/remove gems
$ bundle add rspec
$ bundle remove rspec
# Clean old gems
$ bundle cleanGemspec (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.
# 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.gemRake 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].
# Rakefile
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec
# Custom task
task :greet, [:name] do |t, args|
puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]
# Namespace
namespace :db do
task :migrate do
puts "migrating..."
end
task :seed do
puts "seeding..."
end
end
# $ rake db:migrate
# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
sh "cp #{t.source} #{t.name}"
end
# Multi-line task
task :deploy do
sh 'git push heroku main'
sh 'heroku run rails db:migrate'
endRbenv 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.
# 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 -vRails 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.
# 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
endActiveRecord 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.
# 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.
# 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
endViews 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.
<!-- 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.
# 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, :integerRSpec 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.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Setup with let (lazy, memoized)
let(:user) { User.new(name: "Alice", email: "[email protected]") }
describe '#name' do
it 'returns the name' do
expect(user.name).to eq("Alice")
end
# Multiple expectations with context
context 'when name is blank' do
let(:user) { User.new(name: "") }
it 'is invalid' do
expect(user).not_to be_valid
end
end
end
# One-liner syntax
it { should validate_presence_of(:email) }
it { should have_many(:posts) }
endMocks 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).
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.
# 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).
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 evaluationIntegration 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.
# 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
endFile & 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.
# 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.
# 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.closeDirectory 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.
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 \).
require 'pathname'
require 'tempfile'
# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname # #<Pathname:/home/user/docs>
path.basename # #<Pathname:file.txt>
path.extname # ".txt"
path.parent # #<Pathname:/home/user/docs>
path.join("sub", "file.rb") # /home/user/docs/sub/file.rb
path.exist?
path.directory?
path.file?
path.readable?
# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
f.write("temporary data")
f.rewind
puts f.read
end # file deleted after block
# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink # delete
# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
# work in dir
end # dir deleted afterCSV 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.
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"))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 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 reasonEncoding 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.
# 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_8String 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.
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.
# 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.stringSymbols 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.
# 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)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.
# Module: a namespace + collection of methods
module MathUtils
PI = 3.14159
def self.circle_area(radius) # module method
PI * radius ** 2
end
def square(x) # instance method (for mixins)
x * x
end
end
# Access constants and module methods
MathUtils::PI # 3.14159
MathUtils.circle_area(5) # 78.54
# Namespacing classes
module MyApp
class User
# MyApp::User
end
end
# Prevent instantiation (modules can't be instantiated)
# MathUtils.new # NoMethodErrorInclude vs Extend vs Prepend
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.
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 saveEnumerable 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.
# 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 # falseSingleton 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.
# Singleton method: defined on one object only
str = "hello"
def str.shout
upcase + "!"
end
str.shout # "HELLO!"
# "world".shout # NoMethodError (only str has it)
# Class methods are singleton methods on the class
class Calculator
def self.add(a, b) # singleton method on Calculator
a + b
end
end
Calculator.add(1, 2) # 3
# Singleton class (eigenclass): where singleton methods live
class Calculator
class << self # opens the singleton class
def multiply(a, b) a * b end
def divide(a, b) a / b end
end
end
Calculator.multiply(2, 3) # 6
# Singleton pattern
class Logger
@instance = Logger.new
class << self
attr_reader :instance
end
end
Logger.instance.equal?(Logger.instance) # trueRefinements (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.
# 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 inBlocks 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).
# 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.
# 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
endMethod 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.
# 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.
# 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.
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.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.
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
endOpen 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.
# 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.
class Observable
# Hook: called when class is subclassed
def self.inherited(subclass)
puts "#{subclass} inherits from #{self}"
end
# Hook: called when a module is included
def self.included(base)
puts "#{self} included in #{base}"
end
# Hook: called when a method is added
def method_added(name)
puts "Added method: #{name}"
end
# Hook: called when method is undefined
def method_removed(name)
puts "Removed method: #{name}"
end
end
class Child < Observable
def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method
# Other hooks: method_undefined, extended, prependedeval & 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.
# 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.
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
def save; end
def valid?; true; end
end
u = User.new("Alice", "[email protected]")
# Inspect methods
puts u.methods - Object.methods # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false) # Only this class's methods
puts User.instance_methods(false) # [:save, :valid?, ...]
# Inspect variables
puts u.instance_variables.inspect # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice
# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters
# Check if responds to method
puts u.respond_to?(:save) # true
puts u.is_a?(User) # trueTesting (RSpec/Minitest)
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.
# 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: rspecRSpec 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).
RSpec.describe PaymentProcessor do
let(:gateway) { double('PaymentGateway') }
let(:processor) { PaymentProcessor.new(gateway) }
describe '#charge' do
it 'charges the gateway' do
# Stub: replace method return value
allow(gateway).to receive(:charge).and_return(success: true)
result = processor.charge(100)
expect(result[:success]).to be true
end
it 'raises on gateway error' do
allow(gateway).to receive(:charge)
.and_raise(GatewayError, 'Network failure')
expect { processor.charge(100) }
.to raise_error(GatewayError, /Network/)
end
it 'verifies the gateway was called' do
# Mock: expect method to be called
expect(gateway).to receive(:charge).with(100)
processor.charge(100)
end
it 'receives multiple calls' do
allow(gateway).to receive(:charge).and_return(true, false, true)
expect(gateway.charge(1)).to be true
expect(gateway.charge(2)).to be false
end
end
endMinitest
Minitest 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).
# 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.rbTest 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.
# 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.
# spec/requests/users_spec.rb (integration)
require 'spec_helper'
RSpec.describe 'Users API', type: :request do
describe 'GET /users' do
before { create_list(:user, 3) }
it 'returns all users' do
get '/users', as: :json
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).size).to eq(3)
end
end
describe 'POST /users' do
it 'creates a user' do
post '/users', params: { user: { name: 'Bob', email: '[email protected]' } }
expect(response).to have_http_status(201)
expect(User.last.name).to eq('Bob')
end
end
end
# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'
RSpec.describe 'Login', type: :system do
it 'logs in a user' do
user = create(:user, password: 'password123')
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password123'
click_button 'Log In'
expect(page).to have_content('Welcome')
end
endThreads & 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.
# 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 threadThread 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.
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new
threads = 10.times.map do
Thread.new do
1000.times do
mutex.synchronize { counter += 1 }
end
end
end
threads.each(&:join)
puts counter # 10000 (without mutex, would be less)
# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new
producer = Thread.new do
5.times { |i| queue << "item #{i}" }
queue << :done
end
consumer = Thread.new do
loop do
item = queue.pop
break if item == :done
puts "Processed: #{item}"
end
end
[producer, consumer].each(&:join)
# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.newFibers
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).
# 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 34Async & 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.
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'
Async do
internet = Async::HTTP::Internet.new
# Run requests concurrently
tasks = 3.times.map do |i|
Async do
response = internet.get("https://httpbin.org/delay/#{i}")
puts "Request #{i}: #{response.status}"
end
end
tasks.each(&:wait)
end
# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'
# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value # 42 (blocks until ready)
# Promise chain
Concurrent::Promise.execute { 1 }
.then { |v| v + 1 }
.then { |v| v * 2 }
.then { |v| puts v } # 4
# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdownRactors (Ruby 3.0+)
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.
# 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 safelyEncoding & 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 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.
# 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.
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
endNetwork 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.
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.
# 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')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.
# 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)
endVariable 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.
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}"
endClass & 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.
# 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"
endOne-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.
# 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, 30Practical 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.
# 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
endMetaprogramming
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.
class Foo
define_method(:greet) do |name|
"Hello, #{name}"
end
end
Foo.new.greet("Alice") # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob") # "Hello, Bob"
Foo.new.public_send(:greet, "Bob") # Respects visibilitymethod_missing
method_missing 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.
class DynamicHash
def method_missing(name, *args)
if name.to_s.end_with?('=')
self[name.to_s.chomp('=')] = args.first
else
self[name.to_s]
end
end
def respond_to_missing?(name, include_private = false)
true
end
endeval
eval 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.
# 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.
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflictsClass 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.
class ActiveRecord::Base
def self.has_many(name)
define_method(name) { [] }
end
end
class Post < ActiveRecord::Base
has_many :comments
end
Post.new.comments # []Related Ruby snippets
Copy-paste ready code for common tasks.
Blocks, Procs, Lambdas
Use blocks with yield, Procs, lambdas, and the & operator.
Classes and Modules
Define classes with inheritance, mix in modules, and add class methods.
Iterators
Use each, map, select, reduce, group_by, and lazy enumerators.
Strings
Interpolate, trim, split, replace, and pattern-match strings.
Hashes
Build, default, transform, merge, and group with Hash.
Metaprogramming
Define methods dynamically, intercept with method_missing, and build DSLs.
Error Handling
Raise and rescue typed exceptions with ensure and retry.
File I/O
Read, write, append, traverse directories, and process CSV files.
Was this helpful?