Skip to content

Ruby Reference

Ruby Reference

The Ruby patterns you actually reach for: blocks and iterators, string manipulation, hashes and arrays, modules and mixins, error handling, file I/O, and the standard library gems worth knowing.

Blocks, Procs, Lambdas
# Block — inline code passed to a method
[1, 2, 3].each { |n| puts n }
[1, 2, 3].each do |n|
  puts n * 2
end

# yield — call the block from inside a method
def measure
  start = Time.now
  result = yield          # executes the block
  puts "Took #{Time.now - start}s"
  result
end

measure { sleep 0.1 }

# block_given? — check if block was passed
def greet(name)
  if block_given?
    yield(name)
  else
    "Hello, #{name}"
  end
end

# Explicit block capture with &
def capture(&block)
  block.call(42)         # call the Proc
end

# Proc — saved block, lenient with args, returns from enclosing method
double = Proc.new { |n| n * 2 }
square = proc { |n| n ** 2 }
double.call(5)           # 10
double.(5)               # 10 — shorthand
double[5]                # 10 — shorthand

# Lambda — strict arg checking, returns from lambda only
triple = lambda { |n| n * 3 }
triple = ->(n) { n * 3 }        # stabby lambda (preferred)
triple.call(5)           # 15
triple.lambda?           # true

# Key difference: return behaviour
def test_proc
  p = proc { return "from proc" }
  p.call
  "after proc"           # never reached — proc return exits the method
end

def test_lambda
  l = lambda { return "from lambda" }
  l.call
  "after lambda"         # reached — lambda return stays inside lambda
end

# &method — convert method to block
[1, -2, 3].select(&method(:positive?))   # pass method as block
["1", "2"].map(&method(:Integer))         # convert strings to ints
[nil, false, 1, ""].reject(&:nil?)       # Symbol#to_proc shorthand
Enumerable — the most useful Ruby methods
# map / collect — transform each element
[1, 2, 3].map { |n| n * 2 }             # [2, 4, 6]
["a", "b"].map(&:upcase)                 # ["A", "B"]

# select / filter — keep matching
[1, 2, 3, 4].select { |n| n.even? }     # [2, 4]
[1, 2, nil, false].select(&:itself)      # [1, 2] (truthy only)
[1, 2, nil].compact                      # [1, 2] (remove nil)

# reject — remove matching
[1, 2, 3, 4].reject(&:odd?)             # [2, 4]

# reduce / inject — fold to single value
[1, 2, 3, 4].reduce(:+)                 # 10 (sum)
[1, 2, 3, 4].reduce(10, :+)             # 20 (starting value)
[1, 2, 3].reduce { |acc, n| acc * n }   # 6 (product)

# each_with_object — build a result while iterating
[1, 2, 3].each_with_object([]) { |n, arr| arr << n * 2 }
# vs inject — each_with_object returns the object, inject returns last value

# group_by — partition into a hash
words = ["ant", "bear", "cat", "bee"]
words.group_by(&:length)               # {3=>["ant", "cat"], 4=>["bear", "bee"]}
words.group_by { |w| w[0] }            # {"a"=>["ant"], "b"=>["bear","bee"], "c"=>["cat"]}

# flat_map — map then flatten one level
[[1, 2], [3, 4]].flat_map { |a| a.map { |n| n * 2 } }   # [2, 4, 6, 8]

# each_slice / each_cons
(1..10).each_slice(3).to_a             # [[1,2,3],[4,5,6],[7,8,9],[10]]
(1..5).each_cons(3).to_a              # [[1,2,3],[2,3,4],[3,4,5]]

# zip — pair arrays together
[1, 2, 3].zip([4, 5, 6])               # [[1,4],[2,5],[3,6]]
[1, 2, 3].zip([4, 5, 6]).map { |a, b| a + b }   # [5, 7, 9]

# count / tally
[1, 2, 1, 3, 2, 1].tally              # {1=>3, 2=>2, 3=>1}
[1, 2, 3, 4].count(&:even?)           # 2

# min_by / max_by / minmax_by / sort_by
words.min_by(&:length)                 # "ant"
words.sort_by(&:length)               # ascending by length
words.sort_by { |w| [-w.length, w] }  # length desc, alpha asc

# any? / all? / none? / one?
[1, 2, 3].any? { |n| n > 2 }         # true
[1, 2, 3].all?(&:positive?)           # true
[1, 2, 3].none?(&:negative?)         # true

# find / detect — first match
[1, 2, 3, 4].find { |n| n.even? }    # 2

# chunk — consecutive elements
[1, 1, 2, 2, 3].chunk_while { |a, b| a == b }.to_a   # [[1,1],[2,2],[3]]

# flat_map over nested data
users.flat_map { |u| u.tags }         # all tags from all users, flattened
Hash patterns
# Create and access
h = { name: "Alice", age: 30 }    # symbol keys (most common)
h[:name]                           # "Alice"
h.fetch(:name)                     # "Alice" — raises KeyError if missing
h.fetch(:email, "n/a")            # "n/a" — default value
h.fetch(:email) { |k| "No #{k}" } # block for dynamic default

# Merge — second hash wins on conflict
h.merge({ age: 31, city: "London" })   # new hash
h.merge!(age: 31)                       # in-place

# merge with block — resolve conflicts
defaults.merge(overrides) { |key, old, new_val| old || new_val }

# Transform
h.transform_values { |v| v.to_s }
h.transform_keys(&:to_s)          # symbol → string keys
h.map { |k, v| [k, v * 2] }.to_h  # equivalent to transform_values

# Filter
h.select { |k, v| v.is_a?(Integer) }
h.reject { |k, v| v.nil? }
h.filter_map { |k, v| [k, v * 2] if v.is_a?(Integer) }

# dig — safe nested access (no KeyError)
data = { user: { address: { city: "London" } } }
data.dig(:user, :address, :city)   # "London"
data.dig(:user, :phone, :number)   # nil (not raise)

# each_with_object to build hash
[[:a, 1], [:b, 2]].each_with_object({}) { |(k, v), h| h[k] = v }

# Group and count
words.tally                        # {"ant"=>1, "bear"=>1, ...}

# Default values
counts = Hash.new(0)              # default 0 for missing keys
words.each { |w| counts[w] += 1 }

# to_a, from array
[[:a, 1], [:b, 2]].to_h           # {a: 1, b: 2}
h.to_a                             # [[:name, "Alice"], [:age, 30]]

# Destructuring in block
h.each { |key, value| puts "#{key}: #{value}" }
h.each { |(k, v)| puts "#{k}: #{v}" }  # equivalent
String methods
# Basic
"hello world".upcase              # "HELLO WORLD"
"Hello World".downcase            # "hello world"
"hello world".capitalize          # "Hello world"
"Hello World".swapcase            # "hELLO wORLD"

# Strip whitespace
"  hello  ".strip                 # "hello"
"  hello  ".lstrip                # "hello  "
"  hello  ".rstrip                # "  hello"
"\nhello\n".chomp                 # "\nhello" — remove trailing \n/\r\n
"hello\n".chop                    # "hell" — unconditionally remove last char

# Split and join
"a,b,c".split(",")                # ["a", "b", "c"]
"hello world".split                # ["hello", "world"] (on whitespace)
"hello world".split(" ", 2)       # ["hello", "world"] (max 2 parts)
["a", "b", "c"].join(", ")        # "a, b, c"

# Include / start / end
"hello world".include?("world")   # true
"hello".start_with?("he")         # true
"hello".end_with?("lo")           # true

# Find and replace
"hello world".gsub("o", "0")     # "hell0 w0rld" (all)
"hello world".sub("o", "0")      # "hell0 world" (first)
"hello world".gsub(/\w+/) { |w| w.capitalize }  # "Hello World"
"price: $10".gsub(/\$(\d+)/) { "$#{$1.to_i * 2}" }  # "price: $20"

# Slice / index
"hello"[0]                        # "h"
"hello"[1..3]                     # "ell"
"hello"[-2..]                     # "lo"
"hello"[1, 3]                     # "ell" (start, length)

# Format
"Hello, %s! You are %d." % ["Alice", 30]   # positional
"%-10s | %5d" % ["item", 42]              # padding

# Encoding and conversion
"hello".bytes                     # [104, 101, 108, 108, 111]
"hello".chars                     # ["h", "e", "l", "l", "o"]
42.to_s                           # "42"
"42".to_i                         # 42
"3.14".to_f                       # 3.14
"0xff".to_i(16)                   # 255

# String multiplication
"ha" * 3                          # "hahaha"
"-" * 40                          # "----------------------------------------"

# Heredoc
text = <<~HEREDOC
  Line one
  Line two
HEREDOC
Classes, Modules, Mixins
# Class basics
class Animal
  attr_accessor :name, :age    # getter and setter
  attr_reader :species         # getter only
  attr_writer :nickname        # setter only

  @@count = 0                  # class variable (shared across all instances)

  def initialize(name, species)
    @name = name               # instance variable
    @species = species
    @@count += 1
  end

  def self.count               # class method
    @@count
  end

  def to_s                     # string representation
    "#{@name} (#{@species})"
  end

  def <=>(other)               # comparison for Comparable
    @name <=> other.name
  end
end

# Inheritance
class Dog < Animal
  def initialize(name)
    super(name, "dog")         # call parent initialize
  end

  def speak
    "Woof!"
  end
end

# Modules — namespace OR mixin
module Greetable
  def greet
    "Hello, I'm #{name}"       # expects name method on including class
  end
end

module Trackable
  def track
    "Tracking #{name} at #{Time.now}"
  end
end

class Person
  include Greetable            # adds instance methods
  include Trackable
  extend Greetable             # adds class methods instead
  prepend LoggingModule        # inserted BEFORE the class in lookup chain

  attr_reader :name

  def initialize(name)
    @name = name
  end
end

# Comparable mixin — define <=> once, get <, >, <=, >=, between?, clamp
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.min   # 20°
temps.sort  # [20, 25, 30]

# Struct — quick data class
Point = Struct.new(:x, :y)
p = Point.new(1, 2)
p.x   # 1

# Data — immutable value object (Ruby 3.2+)
Measurement = Data.define(:value, :unit)
m = Measurement.new(value: 100, unit: :kg)
m.value   # 100

# Method visibility
class Example
  def public_method; end        # public by default

  protected
  def protected_method; end     # accessible to instances of same class/subclass

  private
  def private_method; end       # only accessible within the instance
  private :existing_method      # make existing method private
end
Error handling
# begin / rescue / ensure / else
begin
  result = risky_operation
rescue ArgumentError => e
  puts "Bad argument: #{e.message}"
rescue TypeError, ValueError => e
  puts "Type or value error: #{e.message}"
rescue StandardError => e    # catch-all for most errors
  puts "Error: #{e.message}"
  puts e.backtrace.first(5)
else
  puts "Success: #{result}"  # runs ONLY if no exception
ensure
  cleanup()                  # ALWAYS runs (like finally)
end

# Retry with counter
attempts = 0
begin
  attempts += 1
  connect_to_database
rescue ConnectionError => e
  retry if attempts < 3
  raise                      # re-raise after 3 attempts
end

# Custom exceptions
class AppError < StandardError; end
class ValidationError < AppError
  attr_reader :field

  def initialize(msg = "Validation failed", field: nil)
    @field = field
    super(msg)
  end
end

raise ValidationError.new("Invalid email", field: :email)

# rescue in method body (no begin/end needed)
def safe_divide(a, b)
  a / b
rescue ZeroDivisionError
  nil
end

# raise vs raise without args
begin
  do_something
rescue StandardError
  log_error
  raise           # re-raise the same exception with same backtrace
end

# Kernel#raise shortcuts
raise ArgumentError, "message"          # same as raise ArgumentError.new("message")
raise "something went wrong"            # RuntimeError
fail "something went wrong"             # alias for raise

# Exception hierarchy worth knowing:
# Exception
#   └── StandardError (rescue catches this and below)
#         ├── RuntimeError  (raise "msg" default)
#         ├── ArgumentError
#         ├── TypeError
#         ├── NameError → NoMethodError
#         ├── IOError → Errno::* (ENOENT, EACCES...)
#         └── ...
#   ├── ScriptError (SyntaxError, LoadError)
#   └── SignalException (Interrupt from Ctrl+C)
File I/O and common stdlib
# File reading
content = File.read("file.txt")
lines = File.readlines("file.txt", chomp: true)  # no trailing \n

File.open("file.txt") do |f|
  f.each_line { |line| process(line) }
end                          # auto-closes on block exit

# File writing
File.write("out.txt", "content")            # overwrite
File.write("out.txt", "more\n", mode: "a") # append

File.open("out.txt", "w") do |f|
  f.puts "line one"
  f.puts "line two"
end

# Path operations (use Pathname for convenience)
require "pathname"
path = Pathname.new("/var/log/app.log")
path.exist?
path.dirname        # Pathname "/var/log"
path.basename       # Pathname "app.log"
path.extname        # ".log"
path.stem           # "app" (basename without extension)
path.read           # file contents

# File utilities
require "fileutils"
FileUtils.mkdir_p("path/to/dir")   # mkdir -p
FileUtils.cp("src", "dst")
FileUtils.cp_r("src_dir", "dst_dir")   # recursive copy
FileUtils.mv("old", "new")
FileUtils.rm_rf("dir")             # rm -rf — CAREFUL

# Temporary file
require "tempfile"
Tempfile.create("prefix") do |f|
  f.write("data")
  f.rewind
  f.read
end   # auto-deleted on block exit

# JSON
require "json"
JSON.parse('{"name":"Alice","age":30}')   # → Hash
JSON.generate({ name: "Alice", age: 30 }) # → String
JSON.pretty_generate(data)               # formatted

# CSV
require "csv"
CSV.foreach("data.csv", headers: true) do |row|
  puts row["name"]
end

CSV.open("out.csv", "w") do |csv|
  csv << ["name", "age"]
  csv << ["Alice", 30]
end

# HTTP (net/http)
require "net/http"
require "uri"
uri = URI("https://api.example.com/users")
response = Net::HTTP.get_response(uri)
data = JSON.parse(response.body)

# Open3 — run shell commands
require "open3"
stdout, stderr, status = Open3.capture3("ls", "-la")
raise "Failed: #{stderr}" unless status.success?

# Time
Time.now                          # current time
Time.now.utc                      # UTC
Time.now.strftime("%Y-%m-%d")    # "2026-03-14"
Time.parse("2026-01-01")         # parse string (require 'time')
(Time.now - 3600).iso8601        # 1 hour ago, ISO 8601
Bundler and Gems
# Gemfile
source "https://rubygems.org"
ruby "3.3.0"

gem "rails", "~> 7.1"           # >= 7.1.0, < 7.2.0
gem "pg", ">= 1.5"
gem "puma", "~> 6.0"

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "faker"
end

group :development do
  gem "rubocop", require: false
  gem "solargraph"
end

group :test do
  gem "capybara"
  gem "shoulda-matchers"
end

# Bundle commands
bundle install                    # install all gems
bundle install --without production  # skip group
bundle update rails               # update single gem
bundle update                     # update all (careful!)
bundle exec rspec                 # run command in gem context
bundle exec rails server

# Gemfile.lock — always commit this for apps, .gitignore for libraries
bundle lock --update BUNDLER      # update bundler itself

# Version operators
# = 1.0.0   exactly
# != 1.0.0  not this version
# > 1.0.0   greater than
# >= 1.0.0  at least
# < 2.0.0   less than
# ~> 1.0.0  pessimistic constraint (>= 1.0.0, < 1.1.0)
# ~> 1.0    pessimistic constraint (>= 1.0, < 2.0)

# rubocop — Ruby linter
bundle exec rubocop               # check
bundle exec rubocop -a            # auto-correct safe offenses
bundle exec rubocop -A            # auto-correct all (including unsafe)

# gem build / publish
gem build mylib.gemspec
gem push mylib-1.0.0.gem         # requires rubygems.org account
gem yank mylib -v 1.0.0          # retract a version

Track Ruby EOL dates and releases at ReleaseRun.

🔍 Free tool: Ruby Gems Health Checker — check any Ruby gem for latest version and active maintenance before adding to your Gemfile.

🔍 Free tool: Gemfile Batch Health Checker — paste your Gemfile.lock and get health grades for all gems at once.

Founded

2023 in London, UK

Contact

hello@releaserun.com