Ruby Language

SE II Ref
Theme
Class Basics
class Animal
  attr_accessor :name         # get + set
  attr_reader   :species      # get only
  attr_writer   :age          # set only

  def initialize(name, species)
    @name    = name
    @species = species
  end

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

a = Animal.new('Rex', 'Dog')
a.name            # 'Rex'
a.name = 'Max'   # setter
Inheritance
class Dog < Animal
  def initialize(name)
    super(name, 'Canine')
    @tricks = []
  end

  def to_s
    super + " [dog]"
  end
end

dog = Dog.new('Rex')
dog.is_a?(Dog)     # true
dog.is_a?(Animal)  # true
dog.instance_of?(Dog)    # true (exact class)
dog.instance_of?(Animal) # false
dog.class          # Dog
Dog.superclass     # Animal
Class Methods & Variables
class Counter
  @@count = 0             # class variable (shared!)

  def initialize
    @@count += 1
  end

  def self.count
    @@count
  end
end

Counter.new; Counter.new
Counter.count     # 2

# class << self (multiple class methods)
class Foo
  class << self
    def bar; 'bar'; end
    def baz; 'baz'; end
  end
end
Access Control
class Person
  def greet; "Hi, #{full_name}"; end  # public

  protected
  def compare(other)  # same class/subclass only
    name == other.name
  end

  private
  def full_name       # no explicit receiver
    "#{first} #{last}"
  end
end

# Bypass private in tests:
obj.send(:private_method)
💡 protected is rare — mainly for == and <=> between same-class instances.
Comparable
class Box
  include Comparable
  attr_accessor :volume

  def initialize(v); @volume = v; end

  def <=>(other)
    volume <=> other.volume
  end
  # Gives: < > <= >= between? clamp sort min max
end

boxes = [Box.new(3), Box.new(1), Box.new(2)]
boxes.sort.map(&:volume)  # [1, 2, 3]
boxes.min.volume          # 1
method_missing
class Ghost
  def method_missing(name, *args)
    if name.to_s.start_with?('say_')
      word = name.to_s.sub('say_', '')
      "#{word}!"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?('say_') || super
  end
end

Ghost.new.say_hello              # "hello!"
Ghost.new.respond_to?(:say_hi)  # true
⚠ Always pair method_missing with respond_to_missing?
Array Essentials
a = [1, 2, 3]
a = Array.new(3, 0)         # [0, 0, 0]
a = Array.new(3) { |i| i*2 } # [0, 2, 4]

a[0] / a[-1]                 # first / last
a[1, 2]                      # [2,3] start+length
a[1..2]                      # [2,3] range
a.first(2) / a.last(2)

a.push(4) / a << 4          # append
a.pop                        # remove+return last
a.shift / a.unshift(0)       # remove/prepend first
a.flatten                    # nested → flat
a.compact                    # remove nils
a.uniq                       # remove duplicates
a.flatten(1)                 # one level only
Enumerable
arr.map    { |x| x * 2 }     # transform
arr.select { |x| x > 2 }     # filter
arr.reject { |x| x > 2 }     # inverse filter
arr.find   { |x| x > 2 }     # first match
arr.all?   { |x| x > 0 }     # all satisfy?
arr.any?   { |x| x > 5 }     # any satisfy?
arr.none?  { |x| x < 0 }     # none satisfy?
arr.count  { |x| x.even? }   # count matches
arr.reduce(0) { |acc, x| acc + x }
arr.flat_map { |x| [x, x*2] }
arr.each_with_object([]) { |x, a| a << x.to_s }
arr.each_with_index     { |x, i| ... }
arr.zip([4,5,6])          # [[1,4],[2,5],[3,6]]
arr.group_by { |x| x % 2 } # {0=>[2], 1=>[1,3]}
arr.tally                   # {"a"=>2, "b"=>1}
Hash Essentials
h = { name: 'Eric', age: 30 }

h[:name]
h.fetch(:name)              # raises if missing
h.fetch(:x, 'default')
h.dig(:user, :address, :city)

h.merge({ city: 'DC' })
h.merge!({ city: 'DC' })
h.select  { |k,v| v > 18 }
h.reject  { |k,v| v.nil? }
h.transform_values { |v| v.to_s }
h.transform_keys   { |k| k.to_s }
h.slice(:name, :age)
h.each { |k, v| ... }
h.map  { |k, v| [k, v*2] }.to_h
h.any? { |k, v| v > 20 }
Ranges & Sorting
(1..5).to_a      # [1,2,3,4,5] inclusive
(1...5).to_a     # [1,2,3,4]   exclusive
(1..10).cover?(5.5)   # true (no iteration)
(1..10).step(2).to_a # [1,3,5,7,9]

arr.sort
arr.sort_by { |x| x.name }
arr.sort_by { |x| [x.a, x.b] }  # multi-key
arr.sort_by { |x| -x.score }     # descending
arr.sort_by { |x| [-x.priority, x.created_at] } # priority DESC, created_at ASC
arr.sort { |a, b| [b.priority, a.created_at] <=> [a.priority, b.created_at] } # tuple compare
arr.min_by / arr.max_by { |x| x.age }

# Spaceship
1 <=> 2  # -1
2 <=> 2  #  0
3 <=> 2  #  1
Destructuring & Splat
# Parallel assignment
a, b, c       = [1, 2, 3]
first, *rest  = [1, 2, 3, 4]   # rest = [2,3,4]
*init, last   = [1, 2, 3, 4]   # init = [1,2,3]
a, (b, c), d  = [1, [2, 3], 4]  # nested destructure

# Splat in methods
def sum(*nums); nums.sum; end
sum(1, 2, 3)           # 6

# Double splat (keyword args)
def greet(name:, greeting: 'Hello')
  "#{greeting}, #{name}"
end
opts = { name: 'Eric', greeting: 'Hey' }
greet(**opts)          # "Hey, Eric"

# Spread array as args
args = [1, 2, 3]
some_method(*args)
Blocks
# Inline block
[1,2,3].each { |x| puts x }

# do...end (multi-line)
[1,2,3].each do |x|
  puts x * 2
end

# yield
def twice
  yield
  yield
end
twice { puts 'hi' }

# yield with args + guard
def transform(x)
  yield(x) if block_given?
end
transform(5) { |n| n * 2 }  # 10
Proc vs Lambda
# Proc — loose args, returns from enclosing method
p = proc { |x| x * 2 }
p.call(5) / p.(5) / p[5]  # all valid

# Lambda — strict args, returns from lambda only
l = lambda { |x| x * 2 }
l = ->(x) { x * 2 }       # stabby lambda
l.lambda?  # true

# Currying
add = ->(a, b) { a + b }
add5 = add.curry.call(5)
add5.call(3)   # 8
💡 Prefer lambdas — predictable return and strict arity.
& Operator
# Symbol to proc
['a', 'b'].map(&:upcase)     # ['A', 'B']
[1, 2, 3].select(&:odd?)     # [1, 3]
['1','2'].map(&:to_i)         # [1, 2]

# Convert lambda to block
double = ->(x) { x * 2 }
[1,2,3].map(&double)         # [2, 4, 6]

# Capture block as proc
def capture(&block)
  block.call(42)
end
capture { |n| n * 2 }       # 84

# Method objects as blocks
m = method(:puts)
[1,2,3].each(&m)
Proc vs Lambda Quick Reference
ProcLambda
Arity checkNo — extra args silently nilYes — raises ArgumentError
returnReturns from enclosing methodReturns from lambda only
lambda?falsetrue
Creationproc { } / Proc.new { }lambda { } / ->{ }
Best forCallbacks, block captureFunction values, currying
Closures
x = 10
add_x = ->(n) { n + x }
add_x.call(5)   # 15

x = 20
add_x.call(5)   # 25 ← sees updated x!

# Closures capture the binding, not the value
fns = (1..3).map { |i| -> { i * 2 } }
fns[0].call  # 2
fns[1].call  # 4
fns[2].call  # 6
String Methods
s = "Hello, World"
s.upcase / s.downcase / s.swapcase
s.capitalize             # "Hello, world"
s.length / s.size
s.reverse
s.strip / s.lstrip / s.rstrip
s.chomp                   # remove trailing \n
s.chop                    # remove last char
s.include?('World')
s.start_with?('Hello')
s.end_with?('World')
s.split(', ')            # ["Hello", "World"]
s.gsub('l', 'r')        # "Herro, Worrd"
s.gsub(/\d+/, 'NUM')
s.scan(/\w+/)            # array of matches
s.match?(/Hello/)        # true, no MatchData
Interpolation & Heredoc
name = "Eric"
"Hello #{name.upcase}"
'Hello #{name}'         # no interpolation
%Q{Hello #{name}}       # like double-quote
%q{No #{interp}}        # like single-quote

text = <<~HEREDOC
  Line one
  Line #{name}
HEREDOC
# <<~ strips leading whitespace

# Frozen string literal (top of file)
# frozen_string_literal: true
Symbols & Regex
:name.to_s           # "name"
"name".to_sym        # :name
%i[foo bar baz]     # [:foo, :bar, :baz]

# Symbols: same obj always
:foo.object_id == :foo.object_id  # true
"foo".object_id == "foo".object_id # false

# Regex
str =~ /\d+/              # index or nil
m = str.match(/(\d{4})/)
m[1]                       # capture group
m = str.match(/(?<yr>\d{4})/)
m[:yr]                     # named capture
str.gsub(/\d+/) { |n| n.to_i * 2 }
String vs Symbol vs Frozen
StringSymbolFrozen String
MutableYes (default)NoNo
IdentityNew obj each timeSame obj alwaysMay be deduped
GC pressureHigherLower (interned)Lower
Use forUser data, outputKeys, identifiersConstants, keys
Freeze"str".freezeAlways frozen# frozen_string_literal: true
include / extend / prepend
module M
  def hello; "hello from M"; end
end

# include → instance methods
class A; include M; end
A.new.hello

# extend → class methods
class B; extend M; end
B.hello

# extend on single object
obj = Object.new; obj.extend(M)
obj.hello

# prepend → before class in lookup chain
class C; prepend M; end
# M#hello called first; use super to reach C
# Used for: decorators, logging, memoization
ActiveSupport::Concern
module Trackable
  extend ActiveSupport::Concern

  included do
    before_create :set_uuid
    scope :recent, -> { order(created_at: :desc) }
  end

  class_methods do
    def find_by_uuid(uuid)
      find_by!(uuid: uuid)
    end
  end

  def set_uuid
    self.uuid ||= SecureRandom.uuid
  end
end

class Post < ApplicationRecord
  include Trackable
end
Method Lookup Chain
class Child < Parent
  include ModA
  prepend ModB
end

Child.ancestors
# [ModB, Child, ModA, Parent,
#  Object, Kernel, BasicObject]

Child.instance_methods(false) # own only
Child.method_defined?(:foo)   # incl. inherited

# super travels up chain
def foo; super; end    # passes same args
def foo; super(); end  # explicit no args
Enumerable Mixin
class WordCollection
  include Enumerable

  def initialize; @words = []; end
  def add(w); @words << w; end

  def each(&block)
    @words.each(&block)
  end
end

wc = WordCollection.new
wc.add('hello'); wc.add('world')
wc.map(&:upcase)   # ['HELLO', 'WORLD']
wc.sort            # sorted array
wc.min / wc.max
Struct & OpenStruct
# Struct: lightweight value object
Point = Struct.new(:x, :y)
p = Point.new(1, 2)
p.x / p.to_h / p.members   # 1 / {x:1,y:2} / [:x,:y]

# With extra methods
Point = Struct.new(:x, :y) do
  def distance; Math.sqrt(x**2 + y**2); end
end

# Struct equality: value-based
Point.new(1,2) == Point.new(1,2)  # true!

# OpenStruct: dynamic (avoid in perf-critical code)
require 'ostruct'
o = OpenStruct.new(name: 'Eric')
o.anything = 'dynamic!'
Control Flow
puts 'hi' unless admin?
sleep 1 until ready?
return nil if value.blank?

x > 0 ? 'pos' : 'neg'

case obj
when String               then 'string'
when Integer              then 'int'
when /foo/                then 'matches foo'
when (1..10)              then 'in range'
when ->(x) { x > 100 }   then 'big'
else 'other'
end
# case uses === for each when
Exception Handling
begin
  risky_call
rescue ArgumentError => e
  puts e.message
rescue RuntimeError, TypeError => e
  log(e)
  retry              # re-run begin block
else
  puts 'no error'   # runs only if no exception
ensure
  cleanup            # always runs
end

raise ArgumentError, 'bad input'
raise               # re-raise current

class PaymentError < StandardError; end
# ⚠ Rescue StandardError, not Exception
Truthiness & Equality
# ONLY nil and false are falsy!
# 0, "", [], {} are ALL truthy

==           # value equality (overridable)
.equal?     # object identity (same object_id)
.eql?       # strict equality (used in Hash)
===          # case subsumption (when uses this)

1 == 1.0          # true  (coerces)
1.eql?(1.0)       # false (strict)
:foo.equal?(:foo) # true  (interned)
"a".equal?("a")  # false (diff objects)

nil.nil?    # true
false.nil? # false
0.nil?     # false  ← gotcha!
Nil & Safe Navigation
user&.profile&.avatar_url  # nil if any step nil

x.nil?       # only true for nil
x.blank?     # Rails: nil,"","  ",[],{}
x.present?   # Rails: !blank?
x.presence   # returns x or nil if blank

# Default assignment
@val ||= expensive()   # memoize
hash[:key] ||= []      # ensure default

# Null object pattern
user = User.find_by(id: id) || NullUser.new
Common Gotchas
GotchaExplanationFix
0 is truthyOnly nil and false are falsyUse .nil? or .zero? explicitly
array = array + [x]Creates new array, original unchangedUse array << x or array.push(x)
hash.mergeNon-destructive — returns new hashUse merge! to mutate in place
def method(x=[])Default args evaluated once in RubyUse x = nil; x ||= [] inside body
@@class_varShared across ALL subclassesUse @class_instance_var on class instead
p.return in procReturns from enclosing method, not procUse lambda instead of proc
"str" == :symAlways false — different typesUse .to_sym / .to_s to convert
Structure
# spec/models/user_spec.rb
RSpec.describe User do
  describe '#full_name' do
    context 'when first and last name set' do
      it 'returns full name' do
        user = User.new(first: 'Eric', last: 'B')
        expect(user.full_name).to eq('Eric B')
      end
    end

    context 'when last name missing' do
      it 'returns first name only' do
        user = User.new(first: 'Eric')
        expect(user.full_name).to eq('Eric')
      end
    end
  end
end
Setup Hooks
before(:each) { @user = User.create!(name: 'Eric') }
before(:all)  { # runs once before describe block }
after(:each)  { # cleanup per example }
around(:each) { |ex| setup; ex.run; teardown }

# let: lazy, memoized per example
let(:user)  { User.new(name: 'Eric') }
let!(:post) { Post.create!(user: user) }
# let! is eager — runs before each example

# subject: the thing under test
subject { described_class.new(name: 'Eric') }
# described_class → the class in RSpec.describe
💡 Prefer let over before + instance var. let is lazy and re-evaluated each example.
Matchers
# Equality
expect(x).to eq(5)           # ==
expect(x).to eql(5)          # .eql?
expect(x).to equal(y)        # same object
expect(x).to be(nil)         # identity

# Truthiness
expect(x).to be_truthy
expect(x).to be_falsy
expect(x).to be_nil

# Comparison
expect(x).to be > 5
expect(x).to be_between(1, 10)
expect(x).to be_within(0.1).of(3.14)

# Negation
expect(x).not_to eq(5)
expect(x).to_not be_nil
Collection & Type Matchers
# Collections
expect(arr).to include(3)
expect(arr).to include(1, 2)
expect(arr).to contain_exactly(1, 2, 3)   # any order
expect(arr).to match_array([3, 1, 2])      # any order
expect(arr).to have_attributes(size: 3)
expect(arr).to all(be > 0)
expect(hash).to include(name: 'Eric')

# Type
expect(x).to be_a(String)
expect(x).to be_an_instance_of(Hash)
expect(x).to be_kind_of(Numeric)

# Dynamic predicate matchers
expect(user).to be_admin       # user.admin?
expect(str).to be_empty        # str.empty?
expect(obj).to have_key(:name) # obj.has_key?
Doubles & Mocks
# Basic double (strict by default in RSpec 3)
dbl = double('User', name: 'Eric', age: 30)
dbl.name   # 'Eric'

# Instance double (verifies against real class)
dbl = instance_double(User, name: 'Eric')

# Class double
dbl = class_double(User)

# Stub a method
allow(user).to receive(:name).and_return('Eric')
allow(user).to receive(:save).and_return(false)
allow(User).to receive(:find).and_return(user)

# Raise from stub
allow(svc).to receive(:call).and_raise(RuntimeError)
Message Expectations
# expect call to happen (mock)
expect(mailer).to receive(:deliver_now)
expect(user).to receive(:save).once
expect(obj).to  receive(:log).at_least(2).times
expect(obj).to  receive(:call).with('arg')
expect(obj).not_to receive(:delete)

# Spy (stub first, assert after)
spy = spy('mailer')
allow(spy).to receive(:deliver)
# ... exercise code ...
expect(spy).to have_received(:deliver).once

# with matchers
expect(obj).to receive(:go)
  .with(hash_including(name: 'Eric'))
Raising & Changing
# Exceptions
expect { risky }.to raise_error
expect { risky }.to raise_error(ArgumentError)
expect { risky }.to raise_error(/bad input/)
expect { safe  }.not_to raise_error

# Change
expect { User.create!(name: 'x') }
  .to change(User, :count).by(1)

expect { user.update!(age: 31) }
  .to change { user.age }.from(30).to(31)

expect { action }.to change(Post, :count).by_at_least(1)

# Output
expect { puts 'hi' }.to output("hi\n").to_stdout
Shared Examples & Metadata
# Define shared examples
RSpec.shared_examples 'a serializable object' do
  it { expect(subject).to respond_to(:to_json) }
  it { expect(subject).to respond_to(:as_json) }
end

describe User do
  it_behaves_like 'a serializable object'
  it_behaves_like 'a serializable object' do
    let(:subject) { User.new }
  end
end

# Pending / skip
it 'is todo'            # pending (no block)
xit 'skip this' do ...end # skip
it 'skip', skip: 'reason' do ...end
Request Specs (Rails API)
RSpec.describe 'POST /api/v1/users', type: :request do
  let(:valid_params) { { user: { name: 'Eric', email: 'e@x.com' } } }

  it 'creates a user' do
    post '/api/v1/users', params: valid_params, as: :json

    expect(response).to have_http_status(:created)
    expect(JSON.parse(response.body)['name']).to eq('Eric')
  end

  it 'returns errors on invalid data' do
    post '/api/v1/users', params: { user: { name: '' } }, as: :json
    expect(response).to have_http_status(:unprocessable_entity)
    expect(JSON.parse(response.body)).to include('errors')
  end
end
Matchers Quick Reference
MatcherTests
eq(val)Value equality ==
be_nil / be_truthy / be_falsyNil / truthiness
include(x)Array/string/hash contains
match_array([…])Same elements, any order
contain_exactly(…)Exact elements, any order
have_attributes(k: v)Object attribute values
raise_error(Cls)Exception raised
change(obj, :attr).by(n)Attribute changes by n
be_a(Klass)Type check (is_a?)
respond_to(:method)Method defined
have_http_statusRails response status
be_* (dynamic)Calls *? predicate method