← Back to Contents

Chapter 2: Basic Syntax

Variables, types, collections & control flow

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.