DevTools

Cheatsheet Ruby

Referência completa da linguagem Ruby — elegante, expressiva e orientada a objetos

Back to languages
Ruby
64 cards found
Categories:
Versions:

Basic


8 cards
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")  # true

Interpolation #{} 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/arrays

Symbols 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


8 cards
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
end

Iterators 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 0

begin/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


8 cards
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 }  # 5

sort 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


8 cards
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 * 3

Implicit 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
end

Blocks 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 same

Class 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)
end

Proc 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, DSLs

method_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


8 cards
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_h

Struct 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
end

Ruby 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
end

define_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.new

Modules 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
end

Refinements 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 class

public (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


8 cards
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.balance

Rescue 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 exists

Ruby 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.chomp

File.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


8 cards
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.rb

puts 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 result

IRB 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 tasks

Rake 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


8 cards
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
end

Rails 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
end

Sinatra 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 profiling

Benchmark 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 > 5

RSpec 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 Redis

Solargraph 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.