§ 01
Philosophy & Key Differences
Ruby is a fully object-oriented, dynamically typed, interpreted language. If you're coming from Python, JavaScript, or Java, here's what sets Ruby apart:
| Concept | Ruby's Take | vs. Others |
|---|---|---|
| Everything is an object | 1.class → Integernil.class → NilClass | Python primitives, Java int/float are not objects |
| No semicolons needed | Newline ends a statement | JavaScript/Java require ; |
| Blocks | Anonymous code chunks passed to methods | Closer to JS callbacks but with cleaner syntax |
| Symbols | :name — immutable identifiers, not strings | Unique to Ruby (Python has no direct equivalent) |
| Open classes | You can reopen and modify any class, including built-ins | Most languages lock built-in types |
| Expressive conditionals | return if condition (postfix) | Most languages require wrapping in braces |
| Truthiness | Only false and nil are falsy — 0 and "" are truthy! | Python/JS treat 0 and "" as falsy |
§ 02
Setup & Hello World
Installation
# macOS (via Homebrew) brew install ruby # Windows — use RubyInstaller: https://rubyinstaller.org # Linux sudo apt install ruby-full # Recommended: use a version manager (rbenv or rvm) rbenv install 3.3.0 && rbenv global 3.3.0 # Verify ruby --version irb --version # interactive Ruby REPL
Hello World
helloworld.rb# Single-line output puts "Hello, World!" # With string interpolation name = "Ruby" puts "Hello from #{name}!" # print does NOT add a newline; puts does print "No newline here" p "Debugging: shows quotes and type info"
# Run it:
ruby helloworld.rb
§ 03
Core Syntax
Variables & Naming Conventions
| Sigil / Case | Type | Example |
|---|---|---|
| lowercase / snake_case | Local variable | user_name = "Alice" |
@ prefix | Instance variable | @score = 0 |
@@ prefix | Class variable | @@count = 0 |
$ prefix | Global variable | $debug = false |
| UPPER_CASE or CamelCase start | Constant | MAX_SCORE = 100 |
| CamelCase | Class / Module name | class PlayerStats |
Comments
# This is a single-line comment =begin This is a multi-line comment block. Rarely used — most Rubyists prefer # on each line. =end
String Literals
# Double quotes: interpolation + escape sequences greeting = "Hello, #{name}!\n" # Single quotes: literal — NO interpolation, NO \n literal = 'Hello, #{name}!\n' # prints exactly as-is # Heredoc: multi-line strings poem = <<~TEXT Roses are red, Ruby is great. TEXT # Useful string methods "hello".upcase # => "HELLO" " hello ".strip # => "hello" "hello world".split # => ["hello", "world"] "hello".include?("ell") # => true "hello".gsub("l", "r") # => "herro" "42".to_i # => 42 (convert to Integer) 42.to_s # => "42" (convert to String)
Operators
Arithmetic
10 + 3 # => 13 10 - 3 # => 7 10 * 3 # => 30 10 / 3 # => 3 (integer division!) 10.0 / 3 # => 3.333... 10 % 3 # => 1 (modulo) 2 ** 10 # => 1024 (exponent)
Comparison & Logic
== != > < >= <= && || ! and or not # lower precedence # Spaceship operator — returns -1, 0, or 1 3 <=> 5 # => -1 # Safe navigation (no NoMethodError on nil) user&.name # => nil if user is nil
Assignment Shorthand
x += 1 # x = x + 1 x -= 1 x *= 2 x **= 2 # Conditional assignment (very idiomatic Ruby) x ||= 10 # assign 10 only if x is nil or false x &&= 20 # assign 20 only if x is already truthy # Multiple assignment a, b, c = 1, 2, 3 a, b = b, a # swap! (no temp variable needed) first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
§ 04
Data Types
Numbers
42 # Integer 3.14 # Float 1_000_000 # Underscores allowed for readability 0xFF # Hex = 255 0b1010 # Binary = 10 42.even? # => true 7.odd? # => true -5.abs # => 5 3.7.ceil # => 4 3.7.floor # => 3 3.7.round # => 4
Symbols
Symbols are immutable, unique identifiers. Unlike strings, the same symbol is always the same object in memory. Use them for keys, method names, and status flags.
:name # A symbol :status.to_s # => "status" "status".to_sym # => :status # Symbols vs Strings in memory :foo.object_id == :foo.object_id # => true (same object!) "foo".object_id == "foo".object_id # => false (different objects)
Nil, True, False
nil # Ruby's null — it IS an object (NilClass) true # TrueClass false # FalseClass # Only nil and false are falsy. 0 is TRUTHY in Ruby! puts "truthy" if 0 # => prints "truthy" puts "truthy" if "" # => prints "truthy"
§ 05
Control Flow
if / elsif / else / unless
if score >= 90 puts "A" elsif score >= 80 puts "B" else puts "Try again" end # unless = "if not" unless logged_in redirect_to_login end # POSTFIX (one-liners — very idiomatic!) puts "Adult" if age >= 18 puts "Minor" unless age >= 18 # Ternary operator label = score > 50 ? "Pass" : "Fail"
case / when (Switch)
grade = "B" case grade when "A" then puts "Excellent" when "B", "C" then puts "Good" when "D" then puts "Passing" else puts "Failed" end # case uses === (triple equals) — so ranges work! case age when 0..12 then puts "Child" when 13..17 then puts "Teen" when 18.. then puts "Adult" end
Loops
# while i = 0 while i < 5 puts i i += 1 end # until = "while not" until i == 10 i += 1 end # loop (infinite, break manually) loop do input = gets.chomp break if input == "quit" end # for..in (rarely used — prefer iterators) for i in 1..5 puts i end # times, upto, downto — very Ruby-like 5.times { |i| puts i } # 0,1,2,3,4 1.upto(5) { |i| puts i } # 1,2,3,4,5 5.downto(1) { |i| puts i } # 5,4,3,2,1 # Loop control next # like continue — skip to next iteration break # exit the loop redo # restart current iteration without re-checking condition
§ 06
Collections: Arrays & Hashes
Arrays
fruits = ["apple", "banana", "cherry"] mixed = [1, "hello", :ok, true, nil] # any types words = %w[apple banana cherry] # shorthand string array syms = %i[red green blue] # shorthand symbol array # Accessing fruits[0] # => "apple" fruits[-1] # => "cherry" (negative index from end) fruits[0,2] # => ["apple","banana"] (start, length) fruits[1..2] # => ["banana","cherry"] (range) fruits.first # => "apple" fruits.last # => "cherry" # Modifying fruits.push("date") # add to end (also: fruits << "date") fruits.pop # remove from end, returns it fruits.unshift("avocado") # add to front fruits.shift # remove from front fruits.insert(1, "blueberry") # insert at index fruits.delete("banana") # remove by value # Useful methods fruits.length # number of elements fruits.sort # sorted copy fruits.sort! # sort in-place (! = mutates) fruits.reverse fruits.flatten # flatten nested arrays fruits.uniq # remove duplicates fruits.compact # remove nil values fruits.include?("apple") # => true fruits.join(", ") # => "apple, banana, cherry" fruits.count # same as length fruits.sample # random element fruits.shuffle # randomize order # Set operations [1,2,3] + [3,4] # => [1,2,3,3,4] (concatenate) [1,2,3] - [2] # => [1,3] (difference) [1,2] & [2,3] # => [2] (intersection) [1,2] | [2,3] # => [1,2,3] (union)
Hashes (Key-Value Maps)
# Symbol keys (preferred modern style) person = { name: "Alice", age: 30, city: "Atlanta" } # Old "hash rocket" style (any key type) old_style = { "name" => "Alice", 42 => "answer" } # Accessing person[:name] # => "Alice" person[:missing] # => nil (no error) person.fetch(:name) # => "Alice" person.fetch(:missing, "N/A") # => "N/A" (default) # Modifying person[:email] = "alice@example.com" # add/update person.delete(:city) # remove # Useful methods person.keys # => [:name, :age, :email] person.values # => ["Alice", 30, "alice@..."] person.has_key?(:age) # => true person.merge({ role: "admin" }) # returns new merged hash person.each { |key, val| puts "#{key}: #{val}" }
§ 07
Methods
# Basic definition — no 'function' keyword def greet(name) "Hello, #{name}!" # last expression is the return value end # Explicit return def divide(a, b) return nil if b == 0 a.to_f / b end # Default arguments def power(base, exp = 2) base ** exp end power(3) # => 9 power(3, 3) # => 27 # Keyword arguments (named params) def create_user(name:, age:, role: "member") "#{name} (#{age}) - #{role}" end create_user(name: "Bob", age: 25) # Splat — variable number of args def sum(*numbers) numbers.sum # numbers is an Array end sum(1, 2, 3, 4) # => 10 # Double splat — keyword args as hash def configure(**options) options.each { |k, v| puts "#{k} = #{v}" } end # Method name conventions valid? # ? suffix = returns boolean (predicate) save! # ! suffix = dangerous/mutates (bang method)
✦ Tip
In Ruby, parentheses on method calls are optional when unambiguous: puts "hello" and puts("hello") are identical. Most style guides omit parens for simple calls.
§ 08
Blocks, Procs & Lambdas
Blocks are Ruby's most distinctive feature — anonymous chunks of code you pass to a method. Think of them like inline callbacks.
Blocks
# { } for single-line blocks [1, 2, 3].each { |n| puts n } # do..end for multi-line blocks [1, 2, 3].each do |n| squared = n ** 2 puts "#{n} squared = #{squared}" end # Yielding — calling a block from inside a method def repeat(n) n.times { yield } end repeat(3) { puts "Hello!" } # prints 3 times # Pass arguments to block via yield def transform(x) yield(x) if block_given? end transform(5) { |n| n * 2 } # => 10
Key Enumerable Methods (powered by blocks)
nums = [1, 2, 3, 4, 5] nums.map { |n| n * 2 } # => [2,4,6,8,10] (transform) nums.select { |n| n.even? } # => [2,4] (filter) nums.reject { |n| n.even? } # => [1,3,5] (opposite filter) nums.find { |n| n > 3 } # => 4 (first match) nums.all? { |n| n > 0 } # => true (all pass?) nums.any? { |n| n > 4 } # => true (any pass?) nums.none? { |n| n > 10 } # => true nums.count { |n| n.odd? } # => 3 nums.sum # => 15 nums.min # => 1 nums.max # => 5 nums.sort_by { |n| -n } # => [5,4,3,2,1] (descending) nums.flat_map { |n| [n, n] } # => [1,1,2,2,3,3,4,4,5,5] # reduce / inject — fold into a single value nums.reduce(0) { |sum, n| sum + n } # => 15 nums.reduce(:+) # same, using symbol shorthand # each_with_object — build a result while iterating result = nums.each_with_object({}) do |n, hash| hash[n] = n ** 2 end # => {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}
Procs & Lambdas (Stored Blocks)
# Proc — a saved block double = Proc.new { |n| n * 2 } double.call(5) # => 10 double.(5) # shorthand # Lambda — stricter Proc (checks arg count, own return scope) triple = lambda { |n| n * 3 } triple = ->(n) { n * 3 } # => stabby lambda syntax (preferred) triple.call(5) # => 15 # Convert method to block with &method(:name) ["1", "2", "3"].map(&method(:puts)) # & converts a symbol to a block — very idiomatic! ["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"] [1, nil, 2, nil].select(&:itself) # => [1, 2]
§ 09
Object-Oriented Programming
class Animal # Class-level constant KINGDOM = "Animalia" # attr_accessor creates getter + setter methods # attr_reader = read only, attr_writer = write only attr_accessor :name, :age # Constructor def initialize(name, age) @name = name @age = age end # Instance method def speak "..." end # to_s — used when object is printed def to_s "#{@name} (age #{@age})" end # Class method (self. prefix) def self.kingdom KINGDOM end end # Inheritance class Dog < Animal attr_accessor :breed def initialize(name, age, breed) super(name, age) # call parent constructor @breed = breed end def speak "Woof!" end def to_s "#{super} [#{@breed}]" # call parent to_s end end rex = Dog.new("Rex", 3, "Labrador") puts rex # => "Rex (age 3) [Labrador]" puts rex.speak # => "Woof!" puts rex.is_a?(Animal) # => true puts rex.class # => Dog puts Dog.kingdom # => "Animalia"
Comparable & Enumerable (Mixin Modules)
class Box include Comparable attr_accessor :volume def initialize(v) = @volume = v # Define <=> and you get <, >, ==, between?, clamp, sort for free def <=>(other) @volume <=> other.volume end end boxes = [Box.new(5), Box.new(2), Box.new(8)] boxes.sort.map(&:volume) # => [2, 5, 8] boxes.max.volume # => 8
§ 10
Modules & Mixins
Modules serve two purposes: namespacing (grouping related code) and mixins (sharing behavior across classes without inheritance).
# Module as a mixin module Greetable def greet "Hi, I'm #{name}" end end module Farewell def bye "Goodbye from #{name}!" end end class Person include Greetable # adds instance methods include Farewell extend SomeModule # adds class methods (not shown) attr_reader :name def initialize(name) = @name = name end alice = Person.new("Alice") alice.greet # => "Hi, I'm Alice" alice.bye # => "Goodbye from Alice!" # Module as namespace module Payments class Invoice def generate; ...; end end class Receipt def print; ...; end end end inv = Payments::Invoice.new
§ 11
Exception Handling
begin result = 10 / 0 # raises ZeroDivisionError rescue ZeroDivisionError => e puts "Division error: #{e.message}" rescue TypeError, ArgumentError => e puts "Type/Arg error: #{e.message}" rescue => e # catch-all StandardError puts "Unexpected: #{e.message}" else puts "Success! Result = #{result}" # runs only if no exception ensure puts "Always runs (like finally)" end # raise your own exceptions raise ArgumentError, "Age must be positive" if age < 0 # Custom exception class class InsufficientFundsError < StandardError def initialize(amount) super("Need $#{amount} more") end end # retry — inside rescue, attempt the block again attempts = 0 begin attempts += 1 risky_operation rescue NetworkError retry if attempts < 3 end
§ 12
File I/O & User Input
# User input from terminal print "Enter your name: " name = gets.chomp # gets reads a line; chomp strips the \n # Reading files content = File.read("data.txt") # whole file as string lines = File.readlines("data.txt") # array of lines File.foreach("data.txt") do |line| # memory-efficient puts line.chomp end # Writing files File.write("output.txt", "Hello!\n") # write (overwrites) File.open("output.txt", "a") do |f| # "a" = append mode f.puts "Another line" end # file auto-closes after block # Check file/directory existence File.exist?("data.txt") Dir.exist?("./output") Dir.mkdir("./output") Dir.glob("*.rb") # list all .rb files
§ 13 — BEGINNER PROJECT
The Ruby Adventure: A Complete Beginner Program
This program is a mini library system. It deliberately touches: classes with inheritance, modules/mixins, attr_accessor, symbols, hashes, arrays, iterators (map/select/sort_by), blocks, exception handling, file persistence, user input, constants, class methods, predicate methods, and string interpolation. Each section is annotated.
library.rb#!/usr/bin/env ruby # ============================================== # RUBY LIBRARY MANAGEMENT SYSTEM # A comprehensive beginner's Ruby program # ============================================== # ── MODULES ────────────────────────────────── # Modules as mixins: shared behavior across classes module Displayable def display_header(title) width = 50 puts "─" * width puts " #{title}" puts "─" * width end end module Saveable DATA_FILE = "library_data.txt" # module-level constant def save_to_file(data) File.open(DATA_FILE, "w") do |f| data.each { |line| f.puts line } end puts "💾 Saved #{data.length} records to #{DATA_FILE}" end def load_from_file return [] unless File.exist?(DATA_FILE) File.readlines(DATA_FILE).map(&:chomp) end end # ── BASE CLASS ──────────────────────────────── class LibraryItem include Comparable # mixin: gives us <, >, sort include Displayable # our custom mixin attr_accessor :title, :year, :status attr_reader :id # read-only @@item_count = 0 # class variable: shared across all instances def initialize(title, year) @title = title @year = year @status = :available # Symbols as state flags! @@item_count += 1 @id = @@item_count end # Defines sort order via Comparable mixin def <=>(other) @title <=> other.title end # Predicate methods (? suffix = returns boolean) def available? = @status == :available def checked_out? = @status == :checked_out # Class method (called on LibraryItem, not instances) def self.total_items @@item_count end def status_icon @status == :available ? "✅" : "❌" end def to_s "[#{@id}] #{@title} (#{@year}) #{status_icon}" end end # ── SUBCLASSES (Inheritance) ────────────────── class Book < LibraryItem attr_accessor :author, :genre, :pages def initialize(title, author, year, genre: :fiction, pages: 0) super(title, year) # call parent initialize @author = author @genre = genre # keyword arg with default symbol @pages = pages end def long_read? = @pages > 400 def to_s "#{super} | Book by #{@author} | #{@genre} | #{@pages}pp" end # Serialise to a pipe-delimited string for file storage def serialize ["Book", @title, @author, @year, @genre, @pages, @status].join("|") end end class DVD < LibraryItem attr_accessor :director, :runtime_mins def initialize(title, director, year, runtime_mins:) super(title, year) @director = director @runtime_mins = runtime_mins end def runtime_formatted h, m = @runtime_mins.divmod(60) # multiple assignment! "#{h}h #{m}m" end def to_s "#{super} | DVD by #{@director} | #{runtime_formatted}" end def serialize ["DVD", @title, @director, @year, @runtime_mins, @status].join("|") end end # ── LIBRARY CLASS ───────────────────────────── class Library include Displayable include Saveable attr_reader :name def initialize(name) @name = name @items = [] # Array of LibraryItem objects end def add_item(item) @items << item # << is the shovel/append operator puts "✅ Added: #{item.title}" end def checkout(id) item = find_by_id(id) raise ArgumentError, "Item ##{id} not found" unless item raise RuntimeError, "'#{item.title}' is already checked out" if item.checked_out? item.status = :checked_out puts "📤 Checked out: #{item.title}" end def return_item(id) item = find_by_id(id) raise ArgumentError, "Item ##{id} not found" unless item raise RuntimeError, "'#{item.title}' is not checked out" if item.available? item.status = :available puts "📥 Returned: #{item.title}" end # search using select (filter) and any? — block-powered def search(query) query = query.downcase @items.select do |item| item.title.downcase.include?(query) || (item.is_a?(Book) && item.author.downcase.include?(query)) end end def available_items @items.select(&:available?) # & converts symbol to block end def books = @items.select { |i| i.is_a?(Book) } def dvds = @items.select { |i| i.is_a?(DVD) } def sorted = @items.sort # uses <=> from Comparable def by_year = @items.sort_by(&:year) # Statistics using reduce and map def stats { total: @items.count, available: @items.count(&:available?), checked_out: @items.count(&:checked_out?), books: books.count, dvds: dvds.count, avg_book_pages: (books.map(&:pages).sum.to_f / [books.count, 1].max).round(1) } end def display_all display_header("📚 #{@name} — All Items") if @items.empty? puts " No items in library." else sorted.each_with_index do |item, idx| puts " #{idx + 1}. #{item}" end end puts end def display_stats s = stats display_header("📊 Library Statistics") s.each do |key, value| puts " #{key.to_s.ljust(20)} #{value}" end puts end def save save_to_file(@items.map(&:serialize)) end private # methods below are private def find_by_id(id) @items.find { |item| item.id == id } end end # ── MAIN PROGRAM ────────────────────────────── def main lib = Library.new("Ruby City Public Library") # Populate with seed data lib.add_item Book.new("The Pragmatic Programmer", "Hunt & Thomas", 1999, genre: :tech, pages: 352) lib.add_item Book.new("Eloquent Ruby", "Russ Olsen", 2011, genre: :tech, pages: 448) lib.add_item Book.new("Dune", "Frank Herbert", 1965, genre: :scifi, pages: 688) lib.add_item Book.new("1984", "George Orwell", 1949, genre: :dystopia, pages: 328) lib.add_item DVD.new("Blade Runner 2049", "Denis Villeneuve", 2017, runtime_mins: 163) lib.add_item DVD.new("The Matrix", "The Wachowskis", 1999, runtime_mins: 136) puts lib.display_all lib.display_stats # Checkout and return flow with exception handling puts "─" * 50 puts "CHECKOUT DEMO" puts "─" * 50 lib.checkout(1) lib.checkout(3) begin lib.checkout(1) # already checked out → raises RuntimeError rescue RuntimeError => e puts "⚠️ #{e.message}" end begin lib.checkout(99) # doesn't exist → raises ArgumentError rescue ArgumentError => e puts "⚠️ #{e.message}" end lib.return_item(1) puts # Search puts "─" * 50 puts "SEARCH RESULTS FOR 'ruby'" puts "─" * 50 results = lib.search("ruby") results.each { |item| puts " → #{item}" } puts # Functional-style reporting with blocks and iterators puts "─" * 50 puts "LONG READS (books over 400 pages)" puts "─" * 50 lib.books .select(&:long_read?) .sort_by(&:pages) .map { |b| " #{b.title} — #{b.pages} pages" } .each(&method(:puts)) puts # Group by genre using each_with_object (hash building) puts "─" * 50 puts "BOOKS BY GENRE" puts "─" * 50 genres = lib.books.each_with_object(Hash.new { |h, k| h[k] = [] }) do |book, hash| hash[book.genre] << book.title end genres.each do |genre, titles| puts " :#{genre} → #{titles.join(', ')}" end puts # Save to file lib.save # Final updated display lib.display_all lib.display_stats end # Guard clause: only run main if this file is executed directly # (not required/imported by another file) main if __FILE__ == $PROGRAM_NAME
Expected Output
✅ Added: The Pragmatic Programmer
✅ Added: Eloquent Ruby
✅ Added: Dune
✅ Added: 1984
✅ Added: Blade Runner 2049
✅ Added: The Matrix
──────────────────────────────────────────────────
📚 Ruby City Public Library — All Items
──────────────────────────────────────────────────
1. [4] 1984 (1949) ✅ | Book by George Orwell | dystopia | 328pp
2. [5] Blade Runner 2049 (2017) ✅ | DVD by Denis Villeneuve | 2h 43m
3. [3] Dune (1965) ✅ | Book by Frank Herbert | scifi | 688pp
4. [2] Eloquent Ruby (2011) ✅ | Book by Russ Olsen | tech | 448pp
5. [6] The Matrix (1999) ✅ | DVD by The Wachowskis | 2h 16m
6. [1] The Pragmatic Programmer (1999) ✅ | Book by Hunt & Thomas | tech | 352pp
CHECKOUT DEMO
──────────────────────────────────────────────────
📤 Checked out: The Pragmatic Programmer
📤 Checked out: Dune
⚠️ 'The Pragmatic Programmer' is already checked out
⚠️ Item #99 not found
📥 Returned: The Pragmatic Programmer
SEARCH RESULTS FOR 'ruby'
→ [2] Eloquent Ruby (2011) ✅ | Book by Russ Olsen | tech | 448pp
LONG READS (books over 400 pages)
Eloquent Ruby — 448 pages
Dune — 688 pages
BOOKS BY GENRE
:tech → The Pragmatic Programmer, Eloquent Ruby
:scifi → Dune
:dystopia → 1984
💾 Saved 6 records to library_data.txt
📌 What This Program Covers
Classes & Inheritance (LibraryItem → Book, DVD) · Modules/Mixins (Displayable, Saveable, Comparable) · attr_accessor / attr_reader · Symbols as state (:available, :checked_out) · Class variables (@@item_count) · Class methods (self.total_items) · Predicate methods (available?, long_read?) · Keyword arguments (genre:, pages:) · Splat/multi-assign (h, m = divmod) · Blocks & iterators (each, map, select, reject, find, sort_by, each_with_object, each_with_index) · Symbol-to-proc (&:available?) · Exception handling (begin/rescue/raise) · File I/O (File.open, readlines, write) · String interpolation · Guard clause (__FILE__ == $PROGRAM_NAME)
§ 14 — QUICK REFERENCE
Ranges & Other Essentials
Ranges
(1..10) # inclusive (1 to 10) (1...10) # exclusive (1 to 9) ("a".."z") # chars! r = (1..100) r.include?(50) # => true r.min # => 1 r.to_a # to array r.step(5) { |n| p n }
Useful Stdlib
require 'date' Date.today Date.new(2024, 1, 1) require 'json' JSON.parse('{"a":1}') { a: 1 }.to_json require 'set' s = Set.new([1,2,2,3]) # => {1, 2, 3}
Gems (Ruby Packages)
# Install a gem gem install rails gem install colorize # In your code require 'colorize' puts "Hello!".colorize(:red) # Bundler — manage project dependencies bundle init # creates Gemfile bundle install # installs gems from Gemfile bundle exec ruby app.rb
⚡ Next Steps
After mastering these fundamentals: explore Bundler + Gemfile for dependency management, RSpec for testing, Rake for task automation, and ultimately Ruby on Rails or Sinatra for web development. The Ruby community's gem ecosystem is one of the most mature in any language.