Cheatsheet Ruby
Referência completa da linguagem Ruby — elegante, expressiva e orientada a objetos
Ruby
Basic
Variables and Scope
name = "Ruby" # local (lowercase) $global = "everywhere" # global @instance = "object" # instance @@klass = "class" # class CONSTANT = "fixed" # constant (convention) # Multiple assignment: x, y, z = 1, 2, 3 a, b = b, a # elegant swap # Conditional assignment: @cache ||= compute() # only if nil/false # Equivalent: @cache = @cache || compute() # Safe navigation operator: name&.upcase # nil if name is nil # Check if defined: defined?(name) # "local-variable"
The prefix defines the scope: no prefix = local, $ = global, @ = instance, @@ = class, UPPERCASE = constant. ||= for lazy init. &. (safe navigation) avoids errors on nil. Swapping without a temp variable is idiomatic.
Operators
# Arithmetic: + - * / % ** 2 ** 10 # 1024 10 / 3 # 3 (integer division) 10.0 / 3 # 3.333... # Comparison: 1 == 1.0 # true (value) 1.eql?(1.0) # false (value + type) 1.equal?(1) # true (same object) # Logical: && || ! # Safe navigation: name&.upcase # nil if name is nil # Ternary: status = age >= 18 ? "adult" : "minor" # Ranges: (1..10) # inclusive (1 to 10) (1...10) # exclusive (1 to 9) (1..).include?(999) # true (endless)
== compares value, .eql? value+type, .equal? identity. &. (safe navigation) avoids NoMethodError on nil. .. inclusive, ... exclusive. Integer division truncates — use Float for decimals.
Data Types
42.class # Integer
3.14.class # Float
"text".class # String
true.class # TrueClass
nil.class # NilClass
:symbol.class # Symbol
[1, 2].class # Array
{ a: 1 }.class # Hash
(1..10).class # Range
/regex/.class # Regexp
# Everything is an object:
42.even? # true
42.times { } # method on a number
nil.nil? # true
nil.is_a?(Object) # true
# Dynamic but strong typing:
# "42" + 1 # TypeError! (no implicit conversion)Ruby has dynamic but strong typing — no implicit conversions. .class reveals the type. Everything is an object (numbers, nil, true). Symbols (:name) are immutable identifiers. is_a? checks the type hierarchy.
Conversions
"42".to_i # 42
"3.14".to_f # 3.14
42.to_s # "42"
42.to_f # 42.0
"abc".to_i # 0 (does not raise!)
# Strict versions:
Integer("42") # 42
Integer("abc") # ArgumentError!
Float("3.14") # 3.14
# Safe parsing:
begin
n = Integer(input)
rescue ArgumentError
n = 0
end
# Others:
"1,2,3".split(",").map(&:to_i) # [1, 2, 3]
[1, 2, 3].join("-") # "1-2-3"
:name.to_s # "name"to_i/to_f never raise — they return 0 for invalid strings (dangerous!). Integer()/Float() are strict and raise ArgumentError. For safe parsing, use Integer() with rescue.
Strings
name = "World"
greeting = "Hello, #{name}!" # interpolation
# Heredoc (multiline):
text = <<~HEREDOC
Line 1
Line 2
HEREDOC
# Useful methods:
"ruby".upcase # "RUBY"
"RUBY".downcase # "ruby"
"hello world".capitalize # "Hello world"
"a,b,c".split(",") # ["a", "b", "c"]
" abc ".strip # "abc"
"text".reverse # "txet"
"hello".length # 5
"banana".gsub("a", "o") # "bonono"
"test".include?("es") # trueInterpolation #{} is the preferred form. <<~HEREDOC creates multiline strings (strips indent). gsub replaces all occurrences. split splits into an array. strip removes whitespace. Strings are mutable (use .freeze for immutable).
Ranges and Case
# Ranges:
(1..10).to_a # [1, 2, ..., 10]
(1...10).to_a # [1, 2, ..., 9]
("a".."f").to_a # ["a", "b", "c", "d", "e", "f"]
# Check membership:
(1..100).include?(50) # true
(1..100).cover?(50) # true (faster)
# Endless/Beginless (Ruby 2.6+):
(18..) # 18 to infinity
(..100) # infinity up to 100
# case with ranges:
case age
when 0..12 then "child"
when 13..17 then "teenager"
when 18.. then "adult"
end
# Iterate:
(1..5).each { |i| print i } # 12345.. inclusive, ... exclusive. cover? is O(1) (no iteration). Endless (18..) and beginless (..100) ranges. case/when with ranges is idiomatic. Works with numbers, letters and dates.
Symbols and Truthiness
# Symbols: immutable, unique identifiers
:name.object_id == :name.object_id # true
"name".object_id == "name".object_id # false
# Used the hash keys and enums:
person = { name: "Anna", age: 25 }
person[:name] # "Anna"
# Truthiness in Ruby:
# Only nil and false are falsy!
!!nil # false
!!false # false
!!0 # true (0 is truthy!)
!!"" # true (empty string is truthy!)
!![] # true (empty array is truthy!)
# Check nil:
value.nil? # true/false
value.nil? || value.empty? # for strings/arraysSymbols are unique in memory — ideal for keys and enums. In Ruby, only nil and false are falsy — 0, "" and [] are truthy (unlike JS/Python). .nil? checks nil explicitly.
Comments and Documentation
# Line comment =begin Multi-line comment (rarely used) =end # Documentation (RDoc/YARD): # Computes the double of a number. # # @param n [Integer] the number # @return [Integer] the double def double(n) n * 2 end # Annotations: # TODO: implement caching # FIXME: bug with negative values # NOTE: requires Ruby 3.0+ # Generate docs: # yard doc # rdoc lib/
# for line comments. =begin/=end for multi-line (rare). YARD is the documentation standard (@param, @return). Annotations: TODO, FIXME, NOTE. yard doc generates HTML.
Flow Control
If / Unless
if age >= 18 puts "Adult" elsif age >= 12 puts "Teenager" else puts "Child" end # unless (negative if): unless list.empty? process(list) end # Modifier form (one line): puts "Adult" if age >= 18 puts "Empty" unless list.any? # if the expression: result = if x > 0 "positive" else "negative" end
unless is the opposite of if — more readable for negative conditions. The modifier form (code if condition) is idiomatic for one line. if is an expression (returns a value). No parentheses in the condition. Braces replaced by end.
Break, Next and Redo
[1, 2, 3, 4, 5].each do |n| next if n.even? # skips evens (continue) break if n > 4 # stops the loop puts n # 1, 3 end # redo: repeats the current iteration attempts = 0 3.times do |i| attempts += 1 redo if i == 1 && attempts < 5 end # break with value: result = [1, 2, 3].map do |n| break "stopped" if n > 2 n * 2 end # result = "stopped" # retry: restarts the whole begin
next jumps to the next iteration (continue). break ends the loop. redo repeats the current iteration without advancing. break can return a value. retry restarts the whole block. Flexible controls over iteration.
Case / When
case day when 1..5 puts "Weekday" when 6, 7 puts "Weekend" else puts "Invalid" end # With classes (=== operator): case obj when String then "Is text" when Integer then "Is number" when Array then "Is list" end # As expression: status = case code when 200 then :ok when 404 then :not_found when 500 then :error end # With regex: case input when /\d+/ then "number" when /\w+/ then "word" end
case/when uses === for comparison — works with ranges, classes, regexps. It is an expression that returns a value. then allows one line. More powerful than C switch. No break (no fallthrough).
Ternary and ||=
# Ternary: status = age >= 18 ? "adult" : "minor" # || for defaults: name = input || "Anonymous" # ||= (conditional assignment): @cache ||= compute_data() # Only computes if @cache is nil/false # &&= (assign if truthy): value &&= value.strip # Safe navigation (&.): city = user&.address&.city # nil if any level is nil # Combined: total = order&.items&.sum(&:price) || 0
||= is ubiquitous for lazy initialization and caching. &. chains calls without NoMethodError on nil. Ternary for simple conditions. &&= only assigns if truthy. These operators eliminate verbose nil checks.
Loops
# while: n = 10 while n > 0 n -= 1 end # until (negative while): until queue.empty? process(queue.pop) end # infinite loop: loop do break if condition end # Modifier: n -= 1 while n > 0 process until queue.empty? # begin..end while (runs once): begin input = gets end while input.nil?
until runs while the condition is false. loop do creates an infinite loop (stop it with break). The modifier form is idiomatic for short loops. Ruby prefers iterators (each, times) over manual loops.
Pattern Matching (Ruby 3+)
# case/in (Ruby 3.0+):
case data
in { name: String => n, age: Integer => i }
puts "#{n} is #{i} years old"
in [1, 2, *rest]
puts "List with rest: #{rest}"
in { type: "admin" }
puts "Administrator"
else
puts "No match"
end
# Destructuring:
case point
in [x, y] if x == y
puts "Diagonal"
end
# One-line pattern (Ruby 3.0+):
{ name: "Anna" } => { name: }
puts name # "Anna"Pattern matching (Ruby 3.0+) decomposes complex structures. in checks patterns with variable binding. Supports guards (if), splats (*) and pin (^). One-line with =>. Powerful for parsing data and APIs.
Iterators
# times:
5.times { |i| puts i } # 0 1 2 3 4
# each:
[1, 2, 3].each { |n| puts n }
# upto / downto:
1.upto(5) { |i| print i } # 12345
10.downto(1) { |i| print i } # 10987654321
# step:
(0..20).step(5) { |i| print i } # 0 5 10 15 20
# each_with_index:
["a", "b", "c"].each_with_index do |v, i|
puts "#{i}: #{v}"
end
# each_with_object:
hash = [1, 2, 3].each_with_object({}) do |n, h|
h[n] = n ** 2
endIterators are the idiomatic way — they replace for loops. times, upto, downto, step for counting. each_with_index gives the index. each_with_object accumulates into a collection. Blocks {} for one line, do..end for multiple.
Begin / Rescue
begin
result = risky_operation
rescue ArgumentError => e
puts "Invalid argument: #{e.message}"
rescue StandardError => e
puts "Error: #{e.class} - #{e.message}"
else
puts "Success: #{result}"
ensure
puts "Always runs (cleanup)"
end
# Raise:
raise ArgumentError, "Invalid age" if age < 0
# Retry:
begin
connect
rescue TimeoutError
retry # tries again
end
# Modifier:
value = Integer(input) rescue 0begin/rescue is the Ruby try/catch. else runs only without exception. ensure always runs (finally). raise throws. retry restarts. Hierarchy: Exception > StandardError > RuntimeError. Modifier rescue for one line.
Collections
Arrays
nums = [1, 2, 3, 4, 5] nums << 6 # append (push) nums.pop # removes the last nums.shift # removes the first nums.unshift(0) # adds at the start nums[0] # first nums[-1] # last nums[1..3] # slice [2, 3, 4] nums.length # size nums.include?(3) # true # Special literals: %w[one two three] # ["one", "two", "three"] %i[one two three] # [:one, :two, :three] # Heterogeneous: mixed = [1, "two", :three, [4], nil]
Arrays are dynamic and heterogeneous. << is the idiomatic append operator. Negative indexes count from the end. %w[] creates string arrays without quotes. %i[] creates symbol arrays. Slice with ranges. Mutable by default.
Sorting and Searching
nums = [5, 2, 8, 1, 9]
nums.sort # [1, 2, 5, 8, 9]
nums.sort.reverse # [9, 8, 5, 2, 1]
nums.sort_by { |n| -n } # descending
# Searching:
nums.find { |n| n > 4 } # 5 (first)
nums.any? { |n| n > 8 } # true
nums.all? { |n| n > 0 } # true
nums.none? { |n| n > 10 } # true
nums.count { |n| n.even? } # 2
# min/max:
nums.min # 1
nums.max # 9
nums.minmax # [1, 9]
# bsearch (sorted array):
[1, 3, 5, 7, 9].bsearch { |n| n >= 5 } # 5sort returns a new sorted array. sort_by sorts by a custom key. find returns the first match. any?/all?/none? check conditions. bsearch is O(log n) on sorted arrays.
Hashes
person = { name: "Anna", age: 25, city: "Lisbon" }
person[:name] # "Anna"
person.fetch(:email, "N/A") # "N/A" (default)
person[:email] = "ana@mail.com"
person.delete(:city)
person.keys # [:name, :age, :email]
person.values # ["Anna", 25, "ana@mail.com"]
person.each { |k, v| puts "#{k}: #{v}" }
# Hash with default:
counter = Hash.new(0)
counter[:a] += 1 # 1 (no KeyError)
# Hash with default block:
groups = Hash.new { |h, k| h[k] = [] }
groups[:fruits] << "apple"Hashes map keys to values. fetch with a default avoids silent nil. Hash.new(0) for counters. Hash.new { } for groupings. Keys are typically symbols. dig for safe nested navigation.
Group, Zip and Tally
people = [
{ name: "Anna", age: 25 },
{ name: "Ray", age: 30 },
{ name: "Mia", age: 25 }
]
# group_by:
by_age = people.group_by { |p| p[:age] }
# {25=>[Anna,Mia], 30=>[Ray]}
# zip: combines arrays
[1, 2, 3].zip(["a", "b", "c"])
# [[1,"a"], [2,"b"], [3,"c"]]
# partition: splits in two
evens, odds = (1..6).partition(&:even?)
# tally (Ruby 2.7+): counting
"abracadabra".chars.tally
# {"a"=>5, "b"=>2, "r"=>2, "c"=>1, "d"=>1}
# chunk: groups consecutive
[1, 1, 2, 3, 3].chunk_while { |a, b| a == b }.to_a
# [[1,1], [2], [3,3]]group_by groups by key into a Hash of Arrays. zip combines position by position. partition splits in two. tally counts occurrences. chunk_while groups consecutive elements. All return new objects.
Map and Select
nums = [1, 2, 3, 4, 5, 6]
# map: transforms each element
doubles = nums.map { |n| n * 2 } # [2,4,6,8,10,12]
# select/reject: filters
evens = nums.select { |n| n.even? } # [2, 4, 6]
odds = nums.reject { |n| n.even? } # [1, 3, 5]
# Shorthand with &:
strings = nums.map(&:to_s) # ["1", "2", ...]
# Chaining:
result = nums
.select { |n| n > 2 }
.map { |n| n * 10 }
# [30, 40, 50, 60]
# filter_map (Ruby 2.7+):
positives = [-1, 2, -3, 4].filter_map do |n|
n * 2 if n > 0
end # [4, 8]map transforms, select filters (keeps truthy), reject filters (keeps falsy). &:method is idiomatic shorthand. filter_map combines select+map in one pass. Chaining creates declarative pipelines.
Lazy and Infinite
# Lazy: processes only what is needed
result = (1..Float::INFINITY).lazy
.select { |n| n.even? }
.map { |n| n ** 2 }
.first(5)
# [4, 16, 36, 64, 100] — no infinite loop!
# Without lazy: it would try to process infinity
# With lazy: evaluates element by element
# Benchmark: 10M elements
(1..10_000_000).lazy
.select(&:even?)
.map { |n| n * 2 }
.first(10)
# Instant! (only processes 20 elements)
# Infinite Enumerator:
fib = Enumerator.new do |y|
a, b = 0, 1
loop { y << a; a, b = b, a + b }
end
fib.first(10) # [0,1,1,2,3,5,8,13,21,34]lazy defers processing — essential for infinite or large sequences. Evaluates element by element (does not materialize an array). Enumerator.new creates infinite generators. Ideal for pipelines with first(n).
Reduce and Accumulate
nums = [1, 2, 3, 4, 5]
# reduce/inject: accumulates into one value
sum = nums.reduce(0) { |acc, n| acc + n } # 15
sum = nums.reduce(:+) # 15 (shorthand)
product = nums.reduce(1, :*) # 120
# each_with_object: accumulates into a collection
hash = nums.each_with_object({}) do |n, h|
h[n] = n ** 2
end
# {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}
# Grouping with reduce:
words = %w[ana rui ana mia]
freq = words.reduce(Hash.new(0)) do |h, w|
h[w] += 1
h
end # {"ana"=>2, "rui"=>1, "mia"=>1}reduce (alias inject) accumulates elements into a single value. The first arg is the initial value. &:symbol for binary operations. each_with_object avoids an external variable. The base for sum, product, counting, grouping.
Set and Operations
require "set"
a = Set[1, 2, 3, 4]
b = Set[3, 4, 5, 6]
a | b # union: {1,2,3,4,5,6}
a & b # intersection: {3,4}
a - b # difference: {1,2}
a ^ b # symmetric: {1,2,5,6}
a.include?(3) # true (O(1)!)
a.add(5)
a.delete(1)
a.subset?(Set[1, 2, 3, 4, 5])
# Arrays also have operations:
[1, 2, 3] | [3, 4, 5] # [1,2,3,4,5]
[1, 2, 3] & [3, 4, 5] # [3]
[1, 2, 3] - [3] # [1, 2]
# uniq:
[1, 2, 2, 3, 3, 3].uniq # [1, 2, 3]Set has O(1) include? (vs O(n) for Array). Operations: | union, & intersection, - difference, ^ symmetric. Arrays also support |, &, -. uniq removes duplicates.
Methods and Blocks
Methods
def greet(name, prefix = "Hello")
"#{prefix}, #{name}!"
end
greet("Anna") # "Hello, Anna!"
greet("Anna", "Good morning") # "Good morning, Anna!"
# Keyword arguments:
def connect(host:, port: 3306, ssl: false)
# ...
end
connect(host: "localhost", ssl: true)
# Implicit return (last expression):
def double(n)
n * 2 # no return!
end
# Endless method (Ruby 3.0+):
def triple(n) = n * 3Implicit return — the last expression is returned. Keyword args (name:) make calls self-documenting. Endless methods (def x = expr) for one line. Default parameters eliminate overloads. Calling without parentheses is idiomatic.
Method Chaining and tap
# Method chaining:
result = "Hello World"
.downcase
.gsub("world", "ruby")
.capitalize
# "Hello ruby"
# tap: side effect without breaking the chain
[3, 1, 2]
.tap { |a| puts "Original: #{a}" }
.sort
.tap { |a| puts "Sorted: #{a}" }
.first
# then (Ruby 2.6+):
5.then { |n| n * 2 }.then { |n| n + 1 }
# 11
# dig (safe navigation):
data = { user: { address: { city: "Lisbon" } } }
data.dig(:user, :address, :city) # "Lisbon"
data.dig(:user, :phone, :number) # nil (no error!)Method chaining creates readable pipelines. tap allows inspection/logging without interrupting. then transforms the value in the pipeline (like Elixir |>). dig navigates nested hashes without NoMethodError. Everything returns self or a new object.
Blocks and yield
# yield: runs the passed block
def measure_time
start = Time.now
result = yield
puts "Took: #{Time.now - start}s"
result
end
measure_time { slow_operation() }
# Block with parameters:
def repeat_n(n)
n.times { |i| yield(i) }
end
repeat_n(3) { |i| puts "Iteration #{i}" }
# block_given?:
def maybe
yield if block_given?
end
# Blocks {} vs do..end:
[1, 2].each { |n| puts n } # one line
[1, 2].each do |n| # multiple
puts n
puts n * 2
endBlocks are the most idiomatic Ruby feature — anonymous functions passed implicitly. yield runs the block. block_given? checks. {} for one line, do..end for multiple. Used in each, map, open, etc.
Class Methods
class MathUtils
# Class method (self.):
def self.pi
3.14159
end
# Alternative with class << self:
class << self
def root(n)
Math.sqrt(n)
end
end
end
MathUtils.pi # 3.14159
MathUtils.root(16) # 4.0
# Singleton method on an object:
obj = Object.new
def obj.shout
"I AM SPECIAL"
end
obj.shout # "I AM SPECIAL"
# Singleton pattern:
require "singleton"
class DB
include Singleton
end
DB.instance # always the sameClass methods (self.name) are called on the class. class << self opens the singleton class. Singleton methods exist only on one object. include Singleton from the stdlib guarantees a single instance. Useful for factories and utilities.
Procs and Lambdas
# Proc: block the an object
double = Proc.new { |n| n * 2 }
double.call(5) # 10
double.(5) # 10 (shorthand)
# Lambda: strict proc
triple = ->(n) { n * 3 }
triple.call(5) # 15
# Differences:
# 1. Lambda checks arity (arg count)
# 2. return in lambda exits the lambda
# return in proc exits the method
# & to convert block<->proc:
[1, 2, 3].map(&:to_s) # ["1", "2", "3"]
# Equivalent: map { |n| n.to_s }
# Proc the parameter:
def apply(proc, value)
proc.call(value)
endProc and lambda are blocks the objects. Lambda is stricter (checks args, local return). &:method converts a symbol into a proc — idiomatic shorthand. ->(args) { } is the modern lambda syntax. First-class citizens.
Currying and Composition
# Currying:
sum = ->(a, b, c) { a + b + c }
curried = sum.curry
curried[1][2][3] # 6
add5 = curried[5]
add5[3][2] # 10
# Composition (Ruby 2.6+):
double = ->(n) { n * 2 }
increment = ->(n) { n + 1 }
composed = double << increment # double(increment(n))
composed.(5) # 12
composed2 = double >> increment # increment(double(n))
composed2.(5) # 11
# Practical use:
pipeline = ->(s) { s.strip } >> ->(s) { s.downcase }
pipeline.(" HELLO ") # "hello"curry turns an N-arg function into a chain of 1-arg functions. << and >> compose functions (right-to-left / left-to-right). Functional techniques for declarative code. Lambdas are first-class and composable.
Splats and Destructuring
# Splat (*): variable args
def sum(*nums)
nums.reduce(0, :+)
end
sum(1, 2, 3, 4, 5) # 15
# Double splat (**): variable kwargs
def config(**opts)
opts.each { |k, v| puts "#{k}=#{v}" }
end
config(host: "localhost", port: 80)
# Block arg (&):
def with_block(&block)
block.call
end
# Destructuring:
def process((name, age))
"#{name} is #{age}"
end
process(["Anna", 25])
# Splat in assignment:
first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]* collects extra args into an Array. ** collects kwargs into a Hash. & collects the block the a Proc. Destructuring extracts elements from arrays. Splat in assignment splits arrays. Together they create flexible, expressive APIs.
Method Missing and respond_to
class Flexible
def method_missing(name, *args, &block)
"Called #{name} with #{args.inspect}"
end
def respond_to_missing?(name, priv = false)
true
end
end
f = Flexible.new
f.anything(1, 2) # "Called anything with [1, 2]"
f.respond_to?(:abc) # true
# send: calls a method by name
obj.send(:name) # same the obj.name
obj.send("name=", "new") # dynamic setter
# public_send: public methods only
obj.public_send(:name)
# Used in: ActiveRecord, Delegator, DSLsmethod_missing intercepts nonexistent methods — the foundation of ActiveRecord and DSLs. Always implement respond_to_missing? alongside. send calls by name/symbol. public_send respects visibility. Use sparingly.
Classes and Modules
Classes
class Person
attr_accessor :name, :age # getters + setters
attr_reader :email # getter only
def initialize(name, age)
@name = name
@age = age
@email = "#{name}@mail.com"
end
def greeting
"Hello, I am #{@name}"
end
end
p = Person.new("Anna", 25)
p.name = "Mary" # setter via attr_accessor
p.greeting # "Hello, I am Mary"
p.email # "ana@mail.com" (read-only)attr_accessor/attr_reader/attr_writer generate getters/setters automatically. initialize is the constructor (called by .new). @ defines instance variables. Ruby is purely OO — everything is an object.
Structs and Data
# Struct: quick class with attributes
Point = Struct.new(:x, :y) do
def distance
Math.sqrt(x**2 + y**2)
end
end
p = Point.new(3, 4)
p.x # 3
p.distance # 5.0
# Keyword init:
Config = Struct.new(:host, :port, keyword_init: true)
c = Config.new(host: "localhost", port: 80)
# Data (Ruby 3.2+): immutable
Point2 = Data.define(:x, :y)
p2 = Point2.new(x: 3, y: 4)
# p2.x = 5 # NoMethodError! (immutable)
# Both generate: initialize, getters, ==, to_s, to_hStruct creates simple classes without boilerplate. Data (Ruby 3.2+) is immutable — ideal for value objects. Both generate initialize, getters, ==, to_s and to_h automatically. keyword_init: true for named args.
Inheritance
class Animal
def initialize(name)
@name = name
end
def sound
"..."
end
end
class Dog < Animal
def sound
"#{@name}: Woof!"
end
end
class Cat < Animal
def sound
"#{@name}: Meow!"
end
end
Dog.new("Rex").sound # "Rex: Woof!"
# super calls the parent method:
class Puppy < Dog
def sound
super + " (quietly)"
end
endRuby supports single inheritance (<). super calls the parent class method. Methods are overridden without a keyword. There is no multiple class inheritance — use modules (mixins) for behavior composition.
Metaprogramming
# define_method: creates methods dynamically
class Model
[:name, :email, :age].each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |v|
instance_variable_set("@#{attr}", v)
end
end
end
# instance_eval: changes self
config.instance_eval do
@host = "localhost"
@port = 3000
end
# class_eval: evaluates on the class
String.class_eval do
def shout
"#{upcase}!"
end
enddefine_method creates methods at runtime. instance_eval runs a block with self changed — the foundation of DSLs. class_eval evaluates code in the class context. instance_variable_get/set accesses variables dynamically. Powerful but use sparingly.
Modules and Mixins
module Swimmer
def swim
"#{@name} is swimming"
end
end
module Flyer
def fly
"#{@name} is flying"
end
end
class Duck < Animal
include Swimmer # instance methods
include Flyer
end
class Config
extend Swimmer # class methods
end
# Module the namespace:
module API
class Client; end
VERSION = "1.0"
end
API::Client.newModules are the alternative to multiple inheritance. include adds instance methods. extend adds class methods. A module can be included in multiple classes. They also serve the namespaces (API::Client).
Refinements
# Refinements: scoped monkey patch
module StringExtensions
refine String do
def shout
"#{upcase}!"
end
end
end
# Only active in this file:
using StringExtensions
"hello".shout # "HELLO!"
# Monkey patching (global — avoid):
class String
def titleize
split.map(&:capitalize).join(" ")
end
end
# open class (safe extension):
class Array
def second
self[1]
end
endRefinements allow modifying classes with limited scope (using) — avoids global conflicts. Monkey patching modifies globally — powerful but dangerous. Rails uses it extensively. Prefer refinements or modules for extensions.
Access Control
class Account
def initialize(balance)
@balance = balance
end
def balance
@balance
end
private # methods below are private
def validate(value)
value > 0
end
protected # accessible between instances
def compare(other)
@balance <=> other.balance
end
end
# public (default): accessible from outside
# private: no explicit receiver (self)
# protected: between instances of the same classpublic (default): accessible from anywhere. private: only callable without an explicit receiver. protected: allows calls between instances of the same class. In Ruby, privacy is by convention — not strict like Java.
Comparable and Enumerable
# Comparable: implement <=>
class Temperature
include Comparable
attr_reader :celsius
def initialize(celsius)
@celsius = celsius
end
def <=>(other)
celsius <=> other.celsius
end
end
t1 = Temperature.new(25)
t2 = Temperature.new(30)
t1 < t2 # true
t1.between?(Temperature.new(20), Temperature.new(30))
# Enumerable: implement each
class List
include Enumerable
def each(&block)
# iterate elements
end
end
# Gains: map, select, reduce, sort, etc.Comparable + <=> gives all comparison operators. Enumerable + each gives map, select, reduce, sort, etc. Stdlib mixins that add rich behavior with minimal code.
Advanced
Advanced Error Handling
# Exception hierarchy:
# Exception
# ├── StandardError (default rescue)
# │ ├── ArgumentError
# │ ├── RuntimeError
# │ ├── TypeError
# │ ├── NoMethodError
# │ └── IOError
# └── NoMemoryError, SignalException...
# Custom exception:
class InsufficientBalance < StandardError
attr_reader :balance, :amount
def initialize(balance, amount)
@balance = balance
@amount = amount
super("Balance #{balance} insufficient for #{amount}")
end
end
raise InsufficientBalance.new(100, 200)
# Rescue a specific one:
rescue InsufficientBalance => e
puts e.balanceRescue only StandardError (not Exception). Create custom exceptions inheriting from StandardError. super(message) sets the message. Extra attributes for context. The hierarchy allows rescue by category.
Regex
# Matching:
"hello world" =~ /world/ # 6 (index)
"hello world".match?(/\d/) # false
# Capture:
m = "2024-01-15".match(/(\d{4})-(\d{2})-(\d{2})/)
m[1] # "2024"
m[2] # "01"
m[3] # "15"
# Named captures:
m = "Anna:25".match(/(?<name>\w+):(?<age>\d+)/)
m[:name] # "Anna"
m[:age] # "25"
# Substitution:
"abc123".gsub(/\d+/, "X") # "abcX"
"a1b2c3".scan(/\d/) # ["1", "2", "3"]
# Modifiers:
/regex/i # case-insensitive
/regex/m # multiline
/regex/x # verbose (comments)=~ returns the match index. match? returns a boolean (faster). Capture with () and named with (?<name>). gsub replaces, scan extracts all. %r{} the an alternative to //.
Threads and Mutex
# Threads:
threads = 5.times.map do |i|
Thread.new do
sleep(rand)
puts "Thread #{i} finished"
end
end
threads.each(&:join) # waits for all
# Mutex (thread-safety):
mutex = Mutex.new
counter = 0
10.times.map do
Thread.new do
1000.times { mutex.synchronize { counter += 1 } }
end
end.each(&:join)
counter # 10000 (no race condition)
# Queue (thread-safe):
queue = Queue.new
Thread.new { queue.push("work") }
item = queue.pop # blocks until an item existsRuby has a GIL — limited parallelism for CPU-bound work. Mutex protects shared state. Queue is thread-safe for communication. join waits for the thread to finish. For real parallelism, use Ractor (Ruby 3.0+).
IO and Files
# Read a whole file:
content = File.read("data.txt")
# Read line by line:
File.foreach("data.txt") do |line|
puts line.chomp
end
# Write:
File.write("output.txt", "Hello!\n")
# Append:
File.open("log.txt", "a") do |f|
f.puts "New entry"
end
# Check:
File.exist?("data.txt") # true/false
File.size("data.txt") # bytes
File.extname("photo.png") # ".png"
# Dir:
Dir.glob("*.rb") # lists files
Dir.mkdir("new_folder")
Dir.pwd # current directory
# STDIN:
input = gets.chompFile.read reads everything. File.foreach iterates lines (memory-friendly). File.write writes. A block with File.open closes automatically. Dir.glob for patterns. gets reads stdin. chomp removes the newline.
Ractors (Ruby 3+)
# Ractor: real parallelism (no GIL) r1 = Ractor.new do result = heavy_computation result end r2 = Ractor.new do another_heavy_computation end # Results: v1 = r1.take # waits and gets v2 = r2.take # Message-based communication: r = Ractor.new do msg = Ractor.receive msg * 2 end r.send(21) r.take # 42 # Restrictions: # - No shared mutable state # - Objects are copied or frozen # - Ideal for isolated CPU-bound work
Ractors (Ruby 3.0+) allow real parallelism — isolated memory. Message-based communication (send/receive/take). No shared mutable objects. Ideal for CPU-bound work. Alternative: Fibers for cooperative concurrency.
JSON and HTTP
require "json"
require "net/http"
require "uri"
# JSON:
hash = { name: "Anna", age: 25 }
json_str = JSON.generate(hash)
# '{"name":"Anna","age":25}'
data = JSON.parse('{"name":"Anna"}')
data["name"] # "Anna"
# JSON file:
File.write("data.json", JSON.pretty_generate(hash))
data = JSON.parse(File.read("data.json"))
# HTTP GET:
uri = URI("https://api.example.com/data")
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
end
# HTTP POST:
Net::HTTP.post(uri, json_str, "Content-Type" => "application/json")JSON.generate serializes, JSON.parse deserializes. JSON.pretty_generate for readable output. Net::HTTP from the stdlib for requests. Gems: httparty or faraday for more complex APIs. URI parses URLs.
DSLs in Ruby
# DSL with instance_eval:
class HTMLBuilder
def initialize
@html = []
end
def method_missing(name, *args, &block)
@html << "<#{name}>#{args.first}</#{name}>"
end
def to_s
@html.join("\n")
end
end
def html(&block)
b = HTMLBuilder.new
b.instance_eval(&block)
b.to_s
end
html do
h1 "Title"
p "Content"
end
# <h1>Title</h1>
# <p>Content</p>DSLs are ubiquitous in Ruby — Rails, RSpec, Rake, Gemfile. instance_eval + method_missing create declarative APIs. The block runs with self changed. Natural syntax without a receiver. A fundamental pattern of the Ruby ecosystem.
Testing (Minitest)
require "minitest/autorun"
class CalculatorTest < Minitest::Test
def setup
@calc = Calculator.new
end
def test_sum
assert_equal 5, @calc.sum(2, 3)
end
def test_division_by_zero
assert_raises(ZeroDivisionError) do
@calc.divide(1, 0)
end
end
def test_negative
refute @calc.positive?(-1)
end
end
# Run:
# ruby test/calculator_test.rb
# Common assertions:
# assert_equal, assert_nil, assert_includes
# refute, refute_nil, assert_raises
# assert_match(/regex/, string)Minitest ships with the stdlib — zero dependencies. assert_equal compares values. assert_raises checks exceptions. refute negates. setup runs before each test. Rails uses Minitest by default. Alternative: RSpec.
Installation and Setup
Install Ruby (rbenv)
# macOS (Homebrew): brew install rbenv ruby-build rbenv install 3.3.0 rbenv global 3.3.0 # Linux (Ubuntu): sudo apt install rbenv ruby-build rbenv install 3.3.0 rbenv global 3.3.0 # Configure the shell (.bashrc/.zshrc): eval "$(rbenv init -)" # Verify: ruby --version # ruby 3.3.0 (2024-01-01) [x86_64-linux]
rbenv is the recommended version manager. Installs multiple versions side by side. rbenv global sets the default version. rbenv local per project (.ruby-version file). Prefer it over rvm (lighter and simpler).
Gems and Bundler
# Install a gem: gem install httparty gem install rails -v 7.1.0 # List gems: gem list gem list --local # Bundler (dependency management): bundle init # creates the Gemfile # Gemfile: source "https://rubygems.org" gem "rails", "~> 7.1" gem "puma", ">= 5.0" group :test do gem "rspec" end # Commands: bundle install # installs from the Gemfile bundle update # updates gems bundle exec rspec # runs with the gems
gem install installs packages. Bundler manages dependencies per project. Gemfile declares gems, Gemfile.lock pins versions. ~> allows patches. bundle exec guarantees the correct environment. Essential for real projects.
Windows and Docker
# Windows: # 1. Download at rubyinstaller.org # 2. Install with MSYS2 (checkbox) # 3. Verify in the terminal: ruby --version gem --version # Docker: docker run -it ruby:3.3 ruby -e "puts 'Hello'" # Dockerfile: # FROM ruby:3.3-slim # WORKDIR /app # COPY Gemfile* ./ # RUN bundle install # COPY . . # CMD ["ruby", "app.rb"] # WSL2 (recommended for Windows): wsl --install # Then install rbenv inside WSL
Windows: RubyInstaller with MSYS2. Docker has official images (ruby:3.3-slim). WSL2 is recommended for serious development on Windows. Ruby 3.x requires a C compiler for native gems.
Project Structure
my_project/ ├── Gemfile # dependencies ├── Gemfile.lock # exact versions ├── Rakefile # tasks (rake) ├── .ruby-version # Ruby version ├── lib/ │ ├── my_project.rb │ └── my_project/ │ ├── version.rb │ └── core.rb ├── spec/ # tests (RSpec) │ └── core_spec.rb ├── bin/ │ └── console # IRB with the project └── README.md # Rakefile: task :default => :spec task :spec do sh "bundle exec rspec" end
lib/ contains the code. spec/ or test/ for tests. Gemfile + Gemfile.lock for dependencies. .ruby-version pins the version (rbenv). Rakefile defines tasks. bin/console opens IRB with the project loaded.
Hello World and Scripts
# hello.rb
puts "Hello, World!"
# No main() — runs top to bottom
# puts = print with newline
# print = without newline
# Run:
ruby hello.rb
# Script with arguments:
# args.rb
puts "Args: #{ARGV.inspect}"
# ruby args.rb one two three
# Args: ["one", "two", "three"]
# Shebang (Linux/Mac):
#!/usr/bin/env ruby
puts "Executable script"
# chmod +x script.rb
# ./script.rbputs prints with a newline. print without a newline. Scripts run top-level without main(). ARGV is the arguments array. Shebang (#!/usr/bin/env ruby) makes scripts executable. Everything is an object in Ruby.
Require and Load
# require: loads once (stdlib/gems) require "json" require "net/http" require "date" # require_relative: relative path require_relative "lib/my_class" require_relative "../utils/helper" # load: always reloads (dev) load "config.rb" # autoload: lazy loading autoload :Parser, "lib/parser" # Parser is only loaded when used! # In the Gemfile (Bundler): require "bundler/setup" Bundler.require(:default) # Loads all gems from the Gemfile
require loads a file/gem once. require_relative uses a path relative to the file. load always reloads (useful in dev). autoload does lazy loading. Bundler.require loads all Gemfile gems automatically.
IRB (Interactive REPL)
# Start:
$ irb
irb> 2 + 3
=> 5
irb> "ruby".upcase
=> "RUBY"
irb> [1,2,3].map { |n| n * 2 }
=> [2, 4, 6]
irb> exit
# Pry (improved REPL):
gem install pry
$ pry
pry> ls String # lists methods
pry> show-source Array#map
pry> cd [1,2,3] # navigates into the object
# Tips:
# Tab = autocomplete
# _ = last resultIRB is the native Ruby REPL. => shows the result. Pry is an alternative with more features (ls, show-source, cd). Ideal for experimenting and debugging. Tab autocompletes methods.
Rake (Tasks)
# Rakefile
desc "Says hello"
task :hello do
puts "Hello from Rake!"
end
desc "Task with arguments"
task :greet, [:name] do |t, args|
puts "Hello, #{args.name}!"
end
# Dependencies between tasks:
task :build => [:clean, :compile]
task :clean do
puts "Cleaning..."
end
task :compile do
puts "Compiling..."
end
# Run:
rake hello
rake "greet[Anna]"
rake build # runs clean + compile
rake -T # lists tasksRake is the Ruby make. task :name defines tasks. desc adds a description. Dependencies with =>. Arguments with [:arg]. rake -T lists tasks. Rails uses Rake extensively (rake db:migrate).
Ecosystem
Rails (Basics)
# Install:
gem install rails
rails new my_app
cd my_app
rails server # http://localhost:3000
# MVC structure:
# app/models/ → Model (ActiveRecord)
# app/controllers/ → Controller
# app/views/ → View (ERB)
# Generate scaffold:
rails generate scaffold Post title:string body:text
rails db:migrate
# Model:
class Post < ApplicationRecord
validates :title, presence: true
has_many :comments
end
# Controller:
class PostsController < ApplicationController
def index
@posts = Post.all
end
endRails is the most popular Ruby web framework. Convention over configuration. ActiveRecord is the ORM. scaffold generates a full CRUD. rails db:migrate manages the schema. MVC: Models, Controllers, Views. Huge gem ecosystem.
Debugging
# binding.irb (breakpoint): def buggy_method(x) result = x * 2 binding.irb # stops here! result + 1 end # binding.pry (better): # gem "pry" require "pry" def my_method binding.pry end # Pry commands: # ls → local variables # cd object → navigate # show-source → view source # whereami → location # step/next → step-by-step debugging # debug gem (Ruby 3.1+): # gem "debug" require "debug" binding.b # modern breakpoint # Quick debug: p variable # prints with inspect puts variable.inspect
binding.irb / binding.pry create interactive breakpoints. Pry has more features (ls, cd, show-source). Ruby 3.1+ has the native debug gem. p prints with .inspect for quick debugging.
Sinatra (Micro-framework)
# Gemfile:
# gem "sinatra"
require "sinatra"
get "/" do
"Hello, World!"
end
get "/hello/:name" do
"Hello, #{params[:name]}!"
end
post "/data" do
body = request.body.read
"Received: #{body}"
end
# Run:
# ruby app.rb
# http://localhost:4567
# With views (ERB):
get "/page" do
@title = "My Page"
erb :page # views/page.erb
endSinatra is a micro-framework — ideal for small APIs and prototypes. Routes with HTTP verbs (get, post). params accesses parameters. erb renders templates. No ORM — add ActiveRecord or Sequel manually.
Performance
# Benchmark:
require "benchmark"
Benchmark.bm do |x|
x.report("map:") { 1_000_000.times.map { |i| i * 2 } }
x.report("each:") { arr = []; 1_000_000.times.each { |i| arr << i * 2 } }
end
# benchmark-ips (gem):
# gem "benchmark-ips"
Benchmark.ips do |x|
x.report("gsub") { "hello".gsub("l", "r") }
x.report("tr") { "hello".tr("l", "r") }
x.compare!
end
# Performance tips:
# - freeze strings: # frozen_string_literal: true
# - lazy for large collections
# - Set#include? vs Array#include?
# - Avoid allocations in loops
# - ObjectSpace for memory profilingBenchmark from the stdlib for measurements. benchmark-ips for statistical comparisons. # frozen_string_literal: true avoids string allocation. lazy for large collections. Set for O(1) lookups. Avoid allocations in hot paths.
RSpec
# Gemfile: gem "rspec"
# bundle exec rspec --init
# spec/calculator_spec.rb
RSpec.describe Calculator do
describe "#sum" do
it "adds two numbers" do
calc = Calculator.new
expect(calc.sum(2, 3)).to eq(5)
end
it "adds negatives" do
expect(subject.sum(-1, -2)).to eq(-3)
end
end
context "with division" do
it "raises error on zero" do
expect { subject.divide(1, 0) }
.to raise_error(ZeroDivisionError)
end
end
end
# Matchers: eq, be, include, match
# be_truthy, be_nil, be > 5RSpec is the most popular testing framework. Descriptive syntax (describe, context, it). expect().to for assertions. subject is the object under test. Rich matchers: eq, include, raise_error. TDD/BDD friendly.
Deploy and Production
# Dockerfile for a Ruby app: # FROM ruby:3.3-slim # RUN apt-get update && apt-get install -y build-essential # WORKDIR /app # COPY Gemfile* ./ # RUN bundle install --without development test # COPY . . # CMD ["ruby", "app.rb"] # Puma (web server): # gem "puma" # bundle exec puma -p 3000 # Environment variables: # gem "dotenv" require "dotenv/load" DB_URL = ENV["DATABASE_URL"] # Production: # - RACK_ENV=production # - bundle install --without development test # - Use Puma/Unicorn the the server # - Logs: STDOUT (12-factor) # - Health check endpoint
Docker with the ruby:3.3-slim image. Puma is the standard web server. dotenv for environment variables. RACK_ENV=production enables optimizations. Logs to STDOUT (12-factor). Platforms: Heroku, Fly.io, Render, Railway.
RuboCop (Linting)
# Install: gem install rubocop # Run: rubocop # checks everything rubocop app/ # only the app/ folder rubocop -a # auto-fixes # .rubocop.yml (config): AllCops: TargetRubyVersion: 3.3 NewCops: enable Style/StringLiterals: EnforcedStyle: double_quotes Metrics/MethodLength: Max: 20 Layout/LineLength: Max: 120 # Categories: # Style/ → code style # Layout/ → formatting # Lint/ → potential bugs # Metrics/ → complexity
RuboCop is the standard Ruby linter/formatter. It checks style, layout, bugs and complexity. -a auto-fixes. .rubocop.yml configures rules. Integrate it in CI and the editor. Categories: Style, Layout, Lint, Metrics.
Useful Tools
# Solargraph (LSP for editors):
gem install solargraph
# Configure in VS Code / Neovim
# yard (documentation):
gem install yard
yard doc lib/
yard server # http://localhost:8808
# gem "awesome_print":
require "ap"
ap({ name: "Anna", items: [1, 2, 3] })
# Colored, formatted output
# gem "httparty" (simple HTTP):
require "httparty"
response = HTTParty.get("https://api.ex.com/data")
response.parsed_response
# gem "sequel" (SQL builder):
require "sequel"
DB = Sequel.connect("sqlite://db.sqlite3")
DB[:users].where(age: 25).all
# gem "sidekiq" (background jobs):
# Asynchronous processing with RedisSolargraph gives autocomplete/LSP. YARD generates documentation. awesome_print for colored debugging. HTTParty simplifies HTTP. Sequel is a lightweight SQL builder. Sidekiq for background jobs. A rich gem ecosystem for everything.