1. Variables & Types
Ruby is dynamically typed — variables don't have type annotations. Instead, Ruby uses naming conventions and sigils to indicate variable scope.
# Local variable (lowercase, snake_case)
name = "Alice"
age = 30
pi = 3.14159
# Instance variable (prefixed with @)
@user_name = "bob"
# Class variable (prefixed with @@)
@@count = 0
# Global variable (prefixed with $)
$debug_mode = true
# Constants (UPPERCASE or CamelCase)
MAX_RETRIES = 3
API_VERSION = "v2"
DefaultTimeout = 30
| Scope | Prefix | Example | Notes |
|---|---|---|---|
| Local | none |
name |
Visible within current method/block |
| Instance | @ |
@name |
Belongs to an object instance |
| Class | @@ |
@@count |
Shared across all instances of a class |
| Global | $ |
$debug |
Accessible everywhere (use sparingly) |
| Constant | UPPER |
MAX_SIZE |
Starts with uppercase; reassignment triggers a warning |
Core Types
42.class # => Integer
3.14.class # => Float
"hello".class # => String
true.class # => TrueClass
nil.class # => NilClass
:name.class # => Symbol
[1, 2].class # => Array
{a: 1}.class # => Hash
(1..10).class # => Range
# Type checking
42.is_a?(Integer) # => true
42.is_a?(Numeric) # => true
"hi".respond_to?(:upcase) # => true
Everything is an object. In Ruby, even nil, true, false, and integers are objects with methods. There are no "primitive types" like in Java or C.
π Comparison with other languages
Python also has dynamic typing but uses type() to check types. JavaScript uses typeof. Ruby's .class method is available on every object, and .is_a? checks the inheritance chain.
2. Strings
Ruby strings are mutable by default (unlike Python and Java). The two main quote styles behave differently:
# Double quotes: support interpolation and escape sequences
name = "World"
greeting = "Hello, #{name}!\n"
# Single quotes: literal strings, no interpolation
raw = 'Hello, #{name}!\n' # => "Hello, \#{name}!\\n"
# Heredoc for multi-line strings
html = <<~HTML
<div>
<h1>Welcome</h1>
<p>Hello, #{name}</p>
</div>
HTML
# Frozen string (immutable)
frozen = "immutable".freeze
# frozen << " text" # => FrozenError!
Common String Methods
str = "Hello, Ruby World"
str.length # => 17
str.upcase # => "HELLO, RUBY WORLD"
str.downcase # => "hello, ruby world"
str.include?("Ruby") # => true
str.gsub("Ruby", "Crystal") # => "Hello, Crystal World"
str.split(", ") # => ["Hello", "Ruby World"]
str.strip # removes leading/trailing whitespace
str.chars # => ["H", "e", "l", "l", "o", ...]
str.start_with?("He") # => true
str.end_with?("ld") # => true
str[0..4] # => "Hello"
str[-5..] # => "World"
Symbols
Symbols are immutable, interned strings used as identifiers. They're memory-efficient for repeated use:
status = :active
role = :admin
# Symbols are singletons
:hello.object_id == :hello.object_id # => true
"hello".object_id == "hello".object_id # => false
# Converting between String and Symbol
"hello".to_sym # => :hello
:hello.to_s # => "hello"
# Common use: hash keys, method names, enum-like values
user = { name: "Alice", role: :admin }
Tip: Use symbols for identifiers and keys, strings for display text. In Ruby 3.0+, you can add # frozen_string_literal: true at the top of a file to make all string literals frozen by default, improving performance.
3. Arrays
Arrays in Ruby are ordered, integer-indexed collections that can hold any type of object. They are dynamically resizable.
# Creation
nums = [1, 2, 3, 4, 5]
mixed = [1, "two", :three, [4, 5]]
words = %w[apple banana cherry] # => ["apple", "banana", "cherry"]
empty = Array.new(3, 0) # => [0, 0, 0]
# Accessing elements
nums[0] # => 1
nums[-1] # => 5
nums[1..3] # => [2, 3, 4]
nums.first # => 1
nums.last # => 5
Essential Array Methods
arr = [3, 1, 4, 1, 5, 9, 2, 6]
# Adding and removing
arr.push(7) # append: [3, 1, 4, 1, 5, 9, 2, 6, 7]
arr << 8 # also append
arr.pop # remove last, returns 8
arr.unshift(0) # prepend: [0, 3, 1, ...]
arr.shift # remove first, returns 0
# Transformation (returns new arrays)
[1, 2, 3].map { |n| n ** 2 } # => [1, 4, 9]
[1, 2, 3, 4, 5].select { |n| n.odd? } # => [1, 3, 5]
[1, 2, 3, 4, 5].reject { |n| n.odd? } # => [2, 4]
[1, 2, 3, 4, 5].reduce(0) { |sum, n| sum + n } # => 15
# Iteration
["a", "b", "c"].each { |item| puts item }
["a", "b", "c"].each_with_index do |item, i|
puts "#{i}: #{item}"
end
# Useful methods
[3, 1, 4, 1, 5].sort # => [1, 1, 3, 4, 5]
[1, 2, 2, 3, 3].uniq # => [1, 2, 3]
[1, 2, 3].reverse # => [3, 2, 1]
[[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
[1, 2, 3].include?(2) # => true
[1, nil, 2, nil].compact # => [1, 2]
[1, 2, 3].min # => 1
[1, 2, 3].max # => 3
[1, 2, 3].sum # => 6
π Comparison with other languages
Ruby's map/select/reduce correspond to JavaScript's map/filter/reduce and Python's map()/filter()/functools.reduce(). Ruby's block syntax makes chaining these operations natural and readable.
4. Hashes
Hashes are Ruby's key-value data structure, equivalent to dictionaries in Python or objects/Maps in JavaScript.
# Symbol keys (most common, Ruby 3.1+ shorthand)
user = { name: "Alice", age: 30, role: :admin }
# String keys
config = { "host" => "localhost", "port" => 3000 }
# Mixed keys
data = { :id => 1, "name" => "test", 42 => "answer" }
# Accessing values
user[:name] # => "Alice"
user[:email] # => nil
user.fetch(:name) # => "Alice"
user.fetch(:email, "N/A") # => "N/A" (default value)
# Nested access with dig (safe navigation)
nested = { user: { address: { city: "Tokyo" } } }
nested.dig(:user, :address, :city) # => "Tokyo"
nested.dig(:user, :phone, :number) # => nil (no error)
Essential Hash Methods
h = { a: 1, b: 2, c: 3, d: 4 }
h.keys # => [:a, :b, :c, :d]
h.values # => [1, 2, 3, 4]
h.length # => 4
h.key?(:a) # => true (alias: has_key?)
h.value?(2) # => true (alias: has_value?)
# Iteration
h.each { |key, val| puts "#{key}: #{val}" }
h.map { |k, v| [k, v * 10] }.to_h # => {a: 10, b: 20, c: 30, d: 40}
h.select { |k, v| v > 2 } # => {c: 3, d: 4}
h.reject { |k, v| v > 2 } # => {a: 1, b: 2}
# Merging
defaults = { color: "blue", size: "medium" }
overrides = { size: "large", weight: "bold" }
defaults.merge(overrides)
# => {color: "blue", size: "large", weight: "bold"}
# Transform
h.transform_values { |v| v * 100 } # => {a: 100, b: 200, c: 300, d: 400}
h.transform_keys(&:to_s) # => {"a"=>1, "b"=>2, "c"=>3, "d"=>4}
# Destructuring (Ruby 3.1+)
user = { name: "Alice", age: 30, role: :admin }
user => { name:, age: }
puts name # => "Alice"
puts age # => 30
Tip: Prefer dig over chained bracket access for nested hashes. It returns nil instead of raising NoMethodError when a key is missing along the path.
5. Control Flow
if / elsif / else / unless
score = 85
if score >= 90
grade = "A"
elsif score >= 80
grade = "B"
elsif score >= 70
grade = "C"
else
grade = "F"
end
# unless (inverse of if)
unless score < 60
puts "Passed!"
end
# Postfix conditionals (single-line)
puts "Excellent!" if score >= 90
puts "Needs work" unless score >= 60
# Ternary operator
status = score >= 60 ? "pass" : "fail"
case / when
day = "Monday"
case day
when "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
puts "Weekday"
when "Saturday", "Sunday"
puts "Weekend"
else
puts "Unknown"
end
# case with ranges
age = 25
case age
when 0..12 then "child"
when 13..17 then "teenager"
when 18..64 then "adult"
when 65.. then "senior"
end
# case with classes
value = 42
case value
when Integer then "an integer"
when String then "a string"
when Array then "an array"
end
Pattern Matching (Ruby 3.0+)
data = { name: "Alice", age: 30, roles: [:admin, :editor] }
case data
in { name: String => name, roles: [*, :admin, *] }
puts "Admin user: #{name}"
in { name: String => name, age: (18..) => age }
puts "Adult user: #{name}, age #{age}"
end
# Array pattern matching
case [1, 2, 3, 4, 5]
in [1, 2, *rest]
puts "Starts with 1, 2. Rest: #{rest}" # => rest is [3, 4, 5]
end
# Find pattern
case [1, 2, 3, 4, 5]
in [*, 3, *]
puts "Contains 3"
end
# in operator for single-pattern check
if data in { name: /^A/, age: (..35) }
puts "Young user whose name starts with A"
end
Loops
# times
5.times { |i| puts i } # 0, 1, 2, 3, 4
# upto / downto
1.upto(5) { |i| puts i } # 1, 2, 3, 4, 5
5.downto(1) { |i| puts i }
# each (preferred over for)
[10, 20, 30].each { |n| puts n }
(1..10).each { |n| puts n }
# while / until
i = 0
while i < 5
puts i
i += 1
end
j = 0
until j >= 5
puts j
j += 1
end
# loop with break
loop do
input = gets.chomp
break if input == "quit"
puts "You typed: #{input}"
end
# next (skip) and break
(1..10).each do |n|
next if n.even?
break if n > 7
puts n # prints 1, 3, 5, 7
end
π Comparison with other languages
Ruby's unless and postfix conditionals are unique among mainstream languages. Pattern matching (Ruby 3.0+) is similar to Elixir/Rust pattern matching. Ruby has no switch — it uses case/when instead, which can match against types, ranges, and regex.
6. Exception Handling
# Basic exception handling
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Error: #{e.message}"
rescue StandardError => e
puts "Something went wrong: #{e.message}"
ensure
puts "This always runs"
end
# Retry pattern
attempts = 0
begin
attempts += 1
response = Net::HTTP.get(URI("https://api.example.com/data"))
rescue SocketError, Timeout::Error => e
retry if attempts < 3
puts "Failed after 3 attempts: #{e.message}"
end
# Method-level rescue (no begin needed)
def divide(a, b)
a / b
rescue ZeroDivisionError
Float::INFINITY
end
Raising Exceptions
# Raise standard errors
raise "Something went wrong"
raise ArgumentError, "age must be positive"
raise RuntimeError.new("unexpected state")
# Custom exception classes
class InsufficientFundsError < StandardError
attr_reader :amount
def initialize(amount)
@amount = amount
super("Insufficient funds: need #{amount} more")
end
end
def withdraw(balance, amount)
if amount > balance
raise InsufficientFundsError.new(amount - balance)
end
balance - amount
end
begin
withdraw(100, 150)
rescue InsufficientFundsError => e
puts e.message # => "Insufficient funds: need 50 more"
puts e.amount # => 50
end
Best practice: Always rescue specific exception classes rather than bare rescue (which only catches StandardError). Never rescue Exception directly — it catches system-level errors like SignalException and NoMemoryError that should not be swallowed.
π Exception handling comparison
| Language | Try | Catch | Finally |
| Ruby | begin | rescue | ensure |
| Python | try | except | finally |
| JavaScript | try | catch | finally |
| Java | try | catch | finally |
Ruby uniquely offers retry within rescue blocks, making retry patterns trivial to implement.
7. Chapter Summary
π Variables
Dynamic typing. Scope determined by prefix: local, @instance, @@class, $global, CONSTANT. Everything is an object.
π€ Strings
Mutable by default. Double quotes support interpolation #{}. Symbols :name for identifiers. Use freeze for immutability.
π¦ Arrays
push/pop/shift/unshift for mutation. map/select/reduce/each for functional-style iteration.
ποΈ Hashes
Key-value pairs with symbol keys { key: value }. Use dig for safe nested access. merge, transform_values, select/reject.
π Control Flow
if/unless, postfix conditionals, case/when, pattern matching. Loops via each, times, while/until.
β οΈ Exceptions
begin/rescue/ensure. raise to throw. Custom exceptions inherit StandardError. retry for automatic retries.