1. Method Definitions
Ruby methods are defined with def..end. They implicitly return the value of their last expression.
# Basic method
def greet(name)
"Hello, #{name}!"
end
greet("Alice") # => "Hello, Alice!"
# Default parameters
def connect(host, port: 3000, ssl: false)
"#{ssl ? 'https' : 'http'}://#{host}:#{port}"
end
connect("localhost") # => "http://localhost:3000"
connect("api.com", port: 443, ssl: true) # => "https://api.com:443"
# Splat operator for variable arguments
def sum(*numbers)
numbers.reduce(0, :+)
end
sum(1, 2, 3, 4) # => 10
# Double splat for keyword arguments
def create_user(**attrs)
puts attrs.inspect
end
create_user(name: "Alice", age: 30, role: :admin)
# => {:name=>"Alice", :age=>30, :role=>:admin}
Naming Conventions
# snake_case for method names
def calculate_total(items)
items.sum { |item| item[:price] }
end
# Predicate methods end with ?
def valid?(email)
email.include?("@")
end
# Dangerous/mutating methods end with !
name = "hello"
name.upcase # => "HELLO" (returns new string)
name.upcase! # => "HELLO" (modifies in place)
# Setter methods end with =
class User
def name=(new_name)
@name = new_name
end
end
Convention: Methods ending in ? return a boolean, methods ending in ! perform a destructive or dangerous operation, and methods ending in = are setters. These are conventions, not enforced by the language.
π Comparison with other languages
Ruby's implicit return is similar to Rust and Kotlin. Keyword arguments with defaults behave like Python's. The *args and **kwargs splat operators mirror Python's *args and **kwargs exactly.
2. Blocks, Procs & Lambdas
Blocks are Ruby's most distinctive feature — anonymous chunks of code that can be passed to methods. They're the foundation of Ruby's elegant iterator patterns.
Blocks
# Two syntax styles for blocks
[1, 2, 3].each { |n| puts n } # single-line: use { }
[1, 2, 3].each do |n| # multi-line: use do..end
result = n * 10
puts result
end
# yield calls the block passed to a method
def with_logging
puts "[START]"
result = yield
puts "[END]"
result
end
with_logging { 2 + 2 }
# [START]
# [END]
# => 4
# yield with arguments
def transform(value)
yield(value) if block_given?
end
transform(5) { |n| n ** 2 } # => 25
transform(5) # => nil (no block given)
Procs & Lambdas
# Proc: a stored block
square = Proc.new { |n| n ** 2 }
square.call(5) # => 25
square.(5) # => 25 (shorthand)
square[5] # => 25 (another shorthand)
# Lambda: a stricter Proc
multiply = ->(a, b) { a * b }
multiply.call(3, 4) # => 12
multiply.(3, 4) # => 12
# Key differences between Proc and Lambda
# 1. Argument checking
lenient = Proc.new { |a, b| "#{a}, #{b}" }
lenient.call(1) # => "1, " (missing arg becomes nil)
strict = ->(a, b) { "#{a}, #{b}" }
# strict.call(1) # => ArgumentError: wrong number of arguments
# 2. Return behavior
def proc_return
p = Proc.new { return "from proc" }
p.call
"after proc" # never reached
end
def lambda_return
l = -> { return "from lambda" }
l.call
"after lambda" # this IS reached
end
proc_return # => "from proc"
lambda_return # => "after lambda"
The &block Parameter
# Capture a block as a Proc
def apply_twice(&block)
block.call + block.call
end
apply_twice { 10 } # => 20
# Convert a symbol to a block with &
["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
[1, 2, 3, 4].select(&:even?) # => [2, 4]
[1, nil, 2, nil].select(&:nil?) # => [nil, nil]
# Pass a method as a block
def double(n)
n * 2
end
[1, 2, 3].map(&method(:double)) # => [2, 4, 6]
Rule of thumb: Use blocks for simple iteration and callbacks. Use lambdas when you need a callable with strict argument checking. Use Procs rarely — their loose argument handling and non-local return can be surprising.
3. Classes
class Animal
attr_reader :name
attr_accessor :age
def initialize(name, age)
@name = name
@age = age
end
def speak
raise NotImplementedError, "Subclass must implement #speak"
end
def to_s
"#{@name} (age: #{@age})"
end
end
class Dog < Animal
attr_reader :breed
def initialize(name, age, breed)
super(name, age)
@breed = breed
end
def speak
"Woof!"
end
def fetch(item)
"#{@name} fetches the #{item}!"
end
end
dog = Dog.new("Rex", 3, "Labrador")
puts dog.name # => "Rex"
puts dog.speak # => "Woof!"
puts dog.fetch("ball") # => "Rex fetches the ball!"
puts dog # => "Rex (age: 3)"
Access Control
class BankAccount
def initialize(owner, balance)
@owner = owner
@balance = balance
end
def deposit(amount)
validate_amount(amount)
@balance += amount
log_transaction("deposit", amount)
@balance
end
def withdraw(amount)
validate_amount(amount)
raise "Insufficient funds" if amount > @balance
@balance -= amount
log_transaction("withdrawal", amount)
@balance
end
def <=>(other)
@balance <=> other.balance
end
protected
def balance
@balance
end
private
def validate_amount(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
end
def log_transaction(type, amount)
puts "[LOG] #{type}: $#{amount} | Balance: $#{@balance}"
end
end
account = BankAccount.new("Alice", 1000)
account.deposit(500) # => 1500
account.withdraw(200) # => 1300
# account.balance # => NoMethodError (protected)
# account.validate_amount(10) # => NoMethodError (private)
Class Methods & Attributes
class Configuration
@@instances = 0
attr_accessor :debug, :log_level
def initialize
@@instances += 1
@debug = false
@log_level = :info
end
def self.instance_count
@@instances
end
def self.default
config = new
config.debug = false
config.log_level = :warn
config
end
end
c1 = Configuration.new
c2 = Configuration.default
Configuration.instance_count # => 2
π OOP comparison
| Feature | Ruby | Python | Java |
| Inheritance | class Dog < Animal |
class Dog(Animal) |
class Dog extends Animal |
| Constructor | initialize |
__init__ |
Same as class name |
| Self reference | self (implicit) |
self (explicit) |
this (implicit) |
| Multiple inheritance | Mixins (modules) | Yes (MRO) | Interfaces only |
4. Modules & Mixins
Ruby doesn't support multiple inheritance, but modules provide a powerful alternative. Modules serve two purposes: namespacing and mixins.
Namespacing
module Payments
class CreditCard
def charge(amount)
puts "Charging $#{amount} to credit card"
end
end
class PayPal
def charge(amount)
puts "Charging $#{amount} via PayPal"
end
end
end
cc = Payments::CreditCard.new
cc.charge(99.99)
Mixins with include / extend / prepend
module Loggable
def log(message)
puts "[#{self.class}] #{message}"
end
end
module Serializable
def to_json
instance_variables.each_with_object({}) do |var, hash|
hash[var.to_s.delete("@")] = instance_variable_get(var)
end.to_json
end
end
class User
include Loggable # adds instance methods
include Serializable
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
log("User created: #{name}")
end
end
user = User.new("Alice", "alice@example.com")
# => [User] User created: Alice
include vs extend vs prepend
module Greetable
def greet
"Hello from #{self.class}!"
end
end
module ClassGreeting
def description
"I am the #{name} class"
end
end
module Auditable
def save
puts "Audit: saving record..."
super
puts "Audit: record saved."
end
end
class Product
include Greetable # instance methods
extend ClassGreeting # class methods
prepend Auditable # inserts BEFORE the class in lookup chain
def save
puts "Product saved to database"
end
end
Product.new.greet # => "Hello from Product!"
Product.description # => "I am the Product class"
Product.new.save
# Audit: saving record...
# Product saved to database
# Audit: record saved.
Key difference: include adds methods as instance methods. extend adds them as class methods. prepend inserts the module before the class in the method lookup chain, making it ideal for wrapping/decorating existing methods.
Comparable & Enumerable
# Comparable: get <, <=, ==, >=, >, between? for free
class Temperature
include Comparable
attr_reader :degrees
def initialize(degrees)
@degrees = degrees
end
def <=>(other)
@degrees <=> other.degrees
end
end
temps = [Temperature.new(30), Temperature.new(20), Temperature.new(25)]
temps.sort.map(&:degrees) # => [20, 25, 30]
Temperature.new(25).between?(Temperature.new(20), Temperature.new(30)) # => true
# Enumerable: get map, select, reduce, min, max, sort, etc. for free
class NumberSet
include Enumerable
def initialize(*numbers)
@data = numbers
end
def each(&block)
@data.each(&block)
end
end
set = NumberSet.new(5, 3, 8, 1, 9)
set.sort # => [1, 3, 5, 8, 9]
set.select(&:odd?) # => [5, 3, 1, 9]
set.min # => 1
set.reduce(:+) # => 26
5. Built-in Modules
Ruby's standard library provides several powerful modules that you'll use constantly.
Enumerable Highlights
numbers = [4, 8, 15, 16, 23, 42]
numbers.each_slice(2).to_a # => [[4, 8], [15, 16], [23, 42]]
numbers.each_cons(3).to_a # => [[4, 8, 15], [8, 15, 16], ...]
numbers.flat_map { |n| [n, -n] } # => [4, -4, 8, -8, ...]
numbers.zip(["a", "b", "c"]) # => [[4,"a"], [8,"b"], [15,"c"]]
numbers.partition(&:even?) # => [[4, 8, 16, 42], [15, 23]]
numbers.group_by { |n| n > 20 ? :high : :low }
# => {:low=>[4, 8, 15, 16], :high=>[23, 42]}
numbers.tally # counts occurrences (Ruby 2.7+)
%w[a b a c b a].tally # => {"a"=>3, "b"=>2, "c"=>1}
numbers.filter_map { |n| n * 2 if n.even? } # => [8, 16, 32, 84]
numbers.sum # => 108
numbers.minmax # => [4, 42]
numbers.any? { |n| n > 40 } # => true
numbers.all? { |n| n > 0 } # => true
numbers.none? { |n| n < 0 } # => true
numbers.count(&:even?) # => 4
Kernel Methods
# Common Kernel methods available everywhere
puts "Hello" # print with newline
print "Hi " # print without newline
p [1, 2, 3] # inspect output: [1, 2, 3]
pp({a: {b: {c: 1}}}) # pretty-print
# Type conversion
Integer("42") # => 42
Float("3.14") # => 3.14
String(42) # => "42"
Array(nil) # => []
Array(42) # => [42]
Array([1, 2]) # => [1, 2]
# Open a file, URI, or process
open("file.txt") { |f| f.read }
# System commands
system("ls -la") # returns true/false
output = `ls -la` # captures output as string
result = %x(echo hello) # same as backticks
π Comparison with other languages
Ruby's Enumerable module is one of the richest collection APIs in any language. Methods like tally, filter_map, and each_cons have no direct equivalents in Python or JavaScript without additional libraries.
6. Struct & Data
Ruby provides lightweight alternatives to full classes for simple data objects.
Struct
Point = Struct.new(:x, :y)
p1 = Point.new(3, 4)
p1.x # => 3
p1.y # => 4
p1.to_a # => [3, 4]
# Structs support equality by value
p2 = Point.new(3, 4)
p1 == p2 # => true
# Add methods to Struct
Point = Struct.new(:x, :y) do
def distance_to(other)
Math.sqrt((x - other.x) ** 2 + (y - other.y) ** 2)
end
def to_s
"(#{x}, #{y})"
end
end
origin = Point.new(0, 0)
point = Point.new(3, 4)
point.distance_to(origin) # => 5.0
# Struct with keyword_init
Config = Struct.new(:host, :port, :ssl, keyword_init: true)
cfg = Config.new(host: "localhost", port: 3000, ssl: false)
Data Class (Ruby 3.2+)
# Data creates immutable value objects
Coordinate = Data.define(:lat, :lng)
tokyo = Coordinate.new(lat: 35.6762, lng: 139.6503)
tokyo.lat # => 35.6762
tokyo.lng # => 139.6503
# tokyo.lat = 0 # => NoMethodError (immutable!)
# Value equality
paris = Coordinate.new(lat: 48.8566, lng: 2.3522)
tokyo2 = Coordinate.new(lat: 35.6762, lng: 139.6503)
tokyo == tokyo2 # => true
tokyo == paris # => false
# Pattern matching works naturally
case tokyo
in Coordinate[lat: (30..40), lng:]
puts "Located in mid-latitudes, longitude: #{lng}"
end
OpenStruct
require "ostruct"
user = OpenStruct.new(name: "Alice", age: 30)
user.name # => "Alice"
user.email = "alice@example.com" # dynamically add attributes
user.email # => "alice@example.com"
# Useful for quick prototyping, but slower than Struct/Data
# and lacks type safety. Prefer Struct or Data for production code.
When to use which: Use Data (Ruby 3.2+) for immutable value objects. Use Struct for mutable lightweight data containers. Use OpenStruct only for quick prototyping. Use a full class when you need complex behavior or validations.
π Lightweight data objects comparison
| Language | Mutable | Immutable |
| Ruby | Struct |
Data |
| Python | @dataclass |
@dataclass(frozen=True) / NamedTuple |
| Kotlin | data class (var) |
data class (val) |
| Java | POJO | record |
7. Chapter Summary
π§ Methods
Defined with def..end. Implicit return. Default params, keyword args, splat * and double-splat **. Convention: ? for predicates, ! for mutators.
π¦ Blocks & Closures
Blocks via { } or do..end. yield to invoke. Lambdas are strict, Procs are lenient. &:method shorthand for common patterns.
ποΈ Classes
initialize constructor, attr_accessor/reader/writer. Single inheritance with <. public/protected/private access control.
π§© Modules & Mixins
include for instance methods, extend for class methods, prepend for method wrapping. Namespacing with Module::Class.
β‘ Enumerable & Comparable
Include Enumerable + define each to get 50+ collection methods. Include Comparable + define <=> for sorting and comparison.
π Struct & Data
Struct for mutable data objects. Data (Ruby 3.2+) for immutable value objects. OpenStruct for quick prototyping only.