Skip to content

Introduction to Ruby - Wyatt's Notes

Ruby is a dynamic, open-source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write. Ruby was created by Yukihiro Matsumoto (“Matz”) in Japan in 1995 and has since grown into one of the most popular programming languages in the world, powering frameworks like Ruby on Rails, Sinatra, and a vast ecosystem of gems.

Ruby is a pure object-oriented language — everything is an object, including numbers, booleans, and even nil. This consistent object model makes the language cohesive and predictable. Ruby blends ideas from Perl, Smalltalk, Eiffel, Ada, and Lisp to create a language that feels natural and expressive.

Key characteristics of Ruby:

  • Interpreted: Ruby code is executed by the Ruby interpreter (MRI, YJIT, JRuby, TruffleRuby)
  • Dynamically typed: Variable types are determined at runtime
  • Garbage collected: Automatic memory management via mark-and-sweep GC
  • Multi-paradigm: Supports object-oriented, functional, and procedural styles
  • Reflective: Programs can examine and modify their own structure at runtime
  • First public release by Yukihiro Matsumoto
  • Featured a Perl-like syntax with Smalltalk-like object model
  • Included basic features: classes, modules, iterators, closures, exceptions, garbage collection
  • Built-in regular expression support inspired by Perl
  • Early adoption in Japan through community mailing lists
  • Became the dominant Ruby version for nearly a decade
  • Ruby on Rails launched in 2004, driving massive global adoption
  • Introduction of yield blocks as first-class language constructs
  • String handling improved with multibyte character support
  • Performance limitations became apparent as Rails applications scaled
  • YARV (Yet Another Ruby VM) replaced the old interpreter, providing significant performance gains
  • Encoding-aware strings with Encoding class — strings are no longer just byte sequences
  • New hash literal syntax: { key: "value" } (symbol keys)
  • New lambda syntax: -> (x) { x * 2 }
  • Block local variables introduced
  • Fibers added for lightweight cooperative concurrency
  • BasicObject introduced as the root of the class hierarchy
  • Keyword arguments with explicit syntax
  • Refinements introduced (later refined in 2.1+)
  • Module#prepend added for more flexible method overriding
  • Lazy enumerators for chained lazy evaluation
  • __dir__ method returning the directory of the current file
  • to_h convention for converting objects to hashes
  • Rationalised numeric literal syntax: 1/2r, 0.5r
  • Required keyword arguments and keyword argument separation (2.7)
  • Frozen string literals pragma: # frozen_string_literal: true
  • Enumerable#compact, Array#select!, Hash#transform_keys
  • Pattern matching introduced experimentally in 2.7
  • begin/end blocks can omit begin in method bodies
  • Numbered parameters: _1, _2, _3 (2.7)
  • Ractor for true parallel execution without global interpreter lock concerns
  • Fiber Scheduler for non-blocking I/O without changing code structure
  • Pattern matching (in and case/in) became stable
  • One-line pattern matching with =>
  • Rightward assignment: expression => variable
  • Hash#except added
  • Find pattern in pattern matching
  • YJIT (Yet Another JIT) production-ready in 3.2, delivering 2—3x performance improvements
  • Regexp new syntax: /.../ with o option
  • Hash#shift performance improvements
  • Data class for immutable value objects (3.2)
  • Prism parser replacing the old Bison-based parser (3.3)
  • IRB with syntax highlighting and autocompletion by default
  • Improved error messages with suggestions
  • Fiber#scheduler refinements and Async gem ecosystem growth
  • Prism parser replaces the legacy parse.y as default
  • Improved YJIT performance and warmup time
  • it parameter for blocks (experimental)
  • Better error handling and deprecation warnings
  • ReferenceError instead of NameError for certain binding issues

Ruby”s Philosophy: The Principle of Least Astonishment

Section titled “Ruby”s Philosophy: The Principle of Least Astonishment”

The Principle of Least Astonishment (POLA), sometimes called the Principle of Least Surprise, is the guiding philosophy behind Ruby’s design. Matz designed Ruby so that the language should behave in ways that minimise surprise for experienced programmers. When you think you know how something works in Ruby, it likely works exactly that way.

This principle manifests in several ways:

## Everything is an object
5.class # => Integer
5.methods.sort # => Array of methods available on 5
nil.class # => NilClass
true.class # => TrueClass
## Consistent method naming conventions
"hello".length # => 5
[1, 2, 3].length # => 3
{ a: 1, b: 2 }.length # => 2
# Blocks are natural closures
[1, 2, 3].map { |n| n * 2 } # => [2, 4, 6]
[1, 2, 3].select { |n| n > 1 } # => [2, 3]
# nil is handled gracefully in conditionals
name = nil
if name
puts "Has a name"
else
puts "No name" # => "No name" -- nil is falsy
end
# Method naming reflects intent evidently
array.sort # returns new sorted array
array.sort! # sorts in place and returns self
array.empty? # predicate methods end with ?
user.save! # dangerous methods end with !
  1. Developer happiness: Matz famously stated, “I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy.”
  2. Consistency: Similar things should look similar, different things should look different
  3. Expressiveness: The language should enable programmers to express intent evidently
  4. Convenience: Common tasks should be easy, uncommon tasks should be possible

Ruby’s design choices reflect these priorities:

# Optional parentheses for method calls (improved readability)
puts "Hello, World!"
puts("Hello, World!")
# Return value is the last expression evaluated (no explicit return needed)
def greet(name)
"Hello, #{name}!"
end
# Multiple return values via array destructuring
def min_max(array)
[array.min, array.max]
end
min_val, max_val = min_max([3, 1, 4, 1, 5])
# min_val => 1, max_val => 5
# String interpolation is natural
name = "Ruby"
version = 3.3
puts "#{name} #{version} is awesome"
# => "Ruby 3.3 is awesome"

Ruby on Rails (often just “Rails”) is the most prominent Ruby framework. Released in 2004 by David Heinemeier Hansson, Rails popularised many concepts that are now standard in web development:

  • MVC architecture: Model-View-Controller separation of concerns
  • Convention over Configuration: sensible defaults eliminate boilerplate
  • Active Record: Object-relational mapping with a clean API
  • RESTful routing: Resource-based URL mapping
  • Generators: Scaffold controllers, models, migrations, and tests
  • Migration system: Version-controlled database schema changes
# A simple Rails model
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
belongs_to :author
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(5) }
def summary
body.truncate(100)
end
end

Sinatra is a lightweight web framework for Ruby. Unlike Rails, Sinatra provides minimal abstractions, making it ideal for small applications, APIs, and microservices:

# A minimal Sinatra application
require 'sinatra'
get '/' do
'Hello, World!'
end
get '/greet/:name' do
"Hello, #{params['name']}!"
end
post '/items' do
content_type :json
request.body.read.to_json
end

The Ruby ecosystem has over 200,000 gems on RubyGems.org. Some of the most widely used include:

GemPurpose
railsFull-stack web framework
sinatraLightweight web framework
rspecBehaviour-driven testing framework
minitestBuilt-in testing framework with extensions
active_recordORM for relational databases
sequelFlexible database toolkit
sidekiqBackground job processing with Redis
resqueRedis-backed job queue
deviseAuthentication solution for Rails
punditAuthorization library using plain Ruby
factory_botTest fixture replacement
pryRuntime developer console and debugger
dotenvEnvironment variable management
httpartyHTTP client library
nokogiriHTML/XML parsing with XPath/CSS selectors
pgPostgreSQL adapter
redisRedis client
ojFast JSON parser and serializer
rubocopStatic code analyser and formatter
sorbetStatic type checker by Stripe

Ruby has multiple implementations, each with different trade-offs:

MRI (Matz’s Ruby Interpreter): The reference implementation. Written in C. Includes YARV bytecode compiler and YJIT since Ruby 3.2. Most gems are tested against MRI first.

JRuby: Ruby running on the JVM. Provides access to Java libraries and true parallel threads via the JVM. Used when Java ecosystem integration is needed.

TruffleRuby: Ruby on the GraalVM. Uses the Truffle framework for partial evaluation and the Graal compiler to produce highly optimised native code. Can achieve performance competitive with MRI YJIT.

Artichoke: A Ruby implementation in Rust. Focused on compatibility with CRuby. Still experimental.

Ruby.NET: Ruby implementation targeting the .NET runtime.

There are several approaches to installing Ruby. The most common use version managers.

rbenv is a lightweight Ruby version manager that does not compile Ruby itself:

Terminal window
# Install rbenv and ruby-build plugin
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# Add to shell configuration
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
# Install a Ruby version
rbenv install 3.3.0
rbenv global 3.3.0 # set default version
rbenv local 3.3.0 # set per-project version
# Verify
ruby --version # => ruby 3.3.0

rbenv manages Ruby versions using shims — lightweight executables that intercept ruby commands and delegate to the appropriate version. It does not manage gems; that is Bundler’s job.

RVM is a more feature-rich alternative that can also manage gemsets:

Terminal window
# Install RVM
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable
# Install and use a Ruby version
rvm install 3.3.0
rvm use 3.3.0 --default
# Create and use gemsets
rvm gemset create myproject
rvm use 3.3.0@myproject
# List available versions
rvm list known

RVM replaces shell commands with wrapper functions. It is heavier than rbenv but offers gem isolation without external tools.

asdf is a polyglot version manager that handles Ruby, Node.js, Python, and many other runtimes:

Terminal window
# Install asdf
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
# Add Ruby plugin
asdf plugin add ruby
# Install Ruby
asdf install ruby 3.3.0
asdf global ruby 3.3.0
# Per-project version file
echo "ruby 3.3.0" > .tool-versions

Ruby can also be installed via system package managers, though version control is limited:

Terminal window
# macOS
brew install ruby
# Ubuntu/Debian
sudo apt install ruby ruby-dev
# Fedora
sudo dnf install ruby ruby-devel
# Windows (via RubyInstaller)
# Download from https://rubyinstaller.org/

IRB is Ruby’s interactive REPL (Read-Eval-Print Loop) for experimenting with code:

Terminal window
# Start IRB
irb
# Start with a specific Ruby version
irb --version
irb -f # suppress .irbrc loading
# Useful IRB commands
help # show help
irb_info # display IRB version info
ls # list methods (requires 'irb/ext/save-history')
show_source # show source of a method

Starting from Ruby 3.2, IRB includes:

  • Syntax highlighting: Coloured output by default
  • Autocompletion: Tab completion with documentation hints
  • Multi-line editing: Full multi-line code editing support
  • Command history: Persistent history across sessions
# Inside IRB
irb(main):001> "hello".upcase
=> "HELLO"
irb(main):002> [1, 2, 3].reduce(:+)
=> 6
irb(main):003> 5.times { |i| puts i }
0
1
2
3
4
=> 5

Pry is a feature-rich alternative to IRB with runtime introspection:

Terminal window
gem install pry
# Start Pry
pry
# Binding.pry for runtime debugging
def calculate(x, y)
binding.pry # execution pauses here
x + y
end

Pry features include source code browsing, documentation lookup, live object inspection with ls, shell command integration, and gist sharing.

Customise IRB behaviour with an .irbrc file in your home directory:

~/.irbrc
require 'irb/completion'
# Prompt format
IRB.conf[:PROMPT][:MY_PROMPT] = {
PROMPT_I: "ruby> ",
PROMPT_S: "ruby* ",
PROMPT_C: "ruby? ",
RETURN: "=> %s\n"
}
IRB.conf[:PROMPT_MODE] = :MY_PROMPT
# Auto-indent
IRB.conf[:AUTO_INDENT] = true
# History settings
IRB.conf[:SAVE_HISTORY] = 1000
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb_history"
# Save history after each command
IRB.conf[:INSPECT_MODE] = false
# Load libraries
require 'pp'
# Custom helper methods
def y(obj)
puts obj.to_yaml
end
# Quick benchmark helper
def bench(n = 1000)
start = Time.now
n.times { yield }
(Time.now - start) / n
end

RubyGems is Ruby’s package manager. It comes bundled with Ruby and manages gem distribution:

Terminal window
# Search for gems
gem search rails
# Install a gem
gem install rails
# Install a specific version
gem install rails -v 7.1.0
# List installed gems
gem list
gem list --local
# Gem information
gem list rails --details
# Uninstall
gem uninstall rails
# Update all gems
gem update
# Update a specific gem
gem update rails

Bundler provides deterministic dependency management. A Gemfile declares dependencies, and a Gemfile.lock records exact versions:

# Gemfile
source 'https://rubygems.org'
ruby '3.3.0'
# Runtime dependencies
gem 'rails', '~> 7.1.0'
gem 'pg', '~> 1.5'
gem 'puma', '~> 6.0'
gem 'bootsnap', require: false
# Gems used only in specific environments
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'pry-rails'
end
group :test do
gem 'capybara'
gem 'selenium-webdriver'
end
group :development do
gem 'rubocop-rails', require: false
gem 'bullet' # N+1 query detection
end

Version constraint operators in the Gemfile:

gem 'rails', '~> 7.1.0' # >= 7.1.0 and < 7.2.0 (pessimistic)
gem 'rails', '>= 7.1.0' # 7.1.0 or newer
gem 'rails', '~> 7.1' # >= 7.1.0 and < 8.0
gem 'rails', '7.1.0' # exactly 7.1.0
gem 'rails', '> 7.0', '< 8.0' # range constraint
Terminal window
# Install dependencies from Gemfile
bundle install
# Update specific gems
bundle update rails
# Update all gems
bundle update
# Run a command in Bundler context
bundle exec rspec
bundle exec rails server
# Package gems into vendor/cache for offline deployment
bundle package
# Check for outdated gems
bundle outdated
# Show gem versions from Gemfile.lock
bundle show rails
# Execute ruby with Bundler context
bundle exec ruby script.rb
# Create a new gem skeleton
bundle gem my_gem

When creating your own gem, the gemspec defines metadata:

my_gem.gemspec
Gem::Specification.new do |spec|
spec.name = 'my_gem'
spec.version = '0.1.0'
spec.authors = ['Your Name']
spec.email = ['your@email.com']
spec.summary = 'Short description'
spec.description = 'Longer description of the gem'
spec.homepage = 'https://github.com/username/my_gem'
spec.license = 'MIT'
spec.files = Dir.glob('lib/**/*.rb')
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 3.0.0'
spec.add_dependency 'activesupport', '~> 7.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rubocop', '~> 1.0'
end
Terminal window
# Run a Ruby file
ruby program.rb
# Run with flags
ruby -w program.rb # enable warnings
ruby -e 'puts "hello"' # execute one-liner
ruby -c program.rb # syntax check only
ruby --dump=ast program.rb # inspect the AST
ruby --version # show version
script.rb
#!/usr/bin/env ruby
puts "Running Ruby #{RUBY_VERSION}"
def greet(name)
puts "Hello, #{name}!"
end
greet(ARGV[0] || "World")
Terminal window
chmod +x script.rb
./script.rb Alice # => "Hello, Alice!"

Ruby one-liners are powerful for quick text processing and data transformation:

Terminal window
# Print each line with line number
ruby -ne 'puts "#{NR}: #{$_}"' file.txt
# Sum numbers from a file
ruby -ane 'BEGIN{s=0}; s+=$F[0].to_i; END{puts s}' numbers.txt
# In-place edit (like sed -i)
ruby -i -pe 'gsub(/foo/, "bar")' file.txt
# Filter lines matching a pattern
ruby -ne 'print if /pattern/' file.txt
# Split and process CSV-like data
ruby -F, -ane 'puts "#{$F[0]}: #{$F[1]}"' data.csv

Flag explanations:

FlagMeaning
-eExecute one line of script
-nWrap script in while gets; ...; end
-pSame as -n but also prints $_
-aAutosplit mode: splits $_ into $F
-FField separator for autosplit
-iIn-place editing
-lChomp newline, add newline to output
-cCheck syntax only
-wEnable warnings
-dDebug mode

The ~/.ruby-version or .ruby-version file specifies the Ruby version for a project:

3.3.0

This is recognised by rbenv, RVM, and asdf. When multiple developers work on a project, this ensures everyone uses the same Ruby version.

# In your code
RUBY_VERSION # => "3.3.0"
# Gem specification
spec.required_ruby_version = '>= 3.0.0'
# Runtime check
if RUBY_VERSION < '3.0.0'
raise "This gem requires Ruby 3.0 or later"
end

Ruby is like a conversation. Methods are verbs, objects are nouns, and blocks are parenthetical asides. Writing Ruby code feels like speaking a natural language because Yukihiro Matsumoto designed it to be human-readable first and machine-executable second. This philosophy means that Ruby code often reads like English prose.

Everything in Ruby is an object, which is like saying everything in the real world has properties and behaviors. Numbers can tell you if they are even, strings can tell you their length, and even the concept of nothingness (nil) can tell you that it is nil. This object-oriented consistency means you do not need to remember different rules for different types.

Example 1: Idiomatic Ruby Data Transformation

Section titled “Example 1: Idiomatic Ruby Data Transformation”

Problem: Given an array of hashes representing users, filter active users, sort by name, and transform into a formatted string.

users = [
{ name: "Alice", age: 30, active: true },
{ name: "Bob", age: 25, active: false },
{ name: "Charlie", age: 35, active: true },
{ name: "Diana", age: 28, active: true },
]
result = users
.select { |u| u[:active] }
.sort_by { |u| u[:name] }
.map { |u| "#{u[:name]} (#{u[:age]})" }
.join(", ")
puts result
# => "Alice (30), Charlie (35), Diana (28)"

Explanation: Ruby’s enumerable methods chain logically. select filters by the :active key, sort_by orders alphabetically by name, map transforms each hash into a formatted string, and join combines them. This functional pipeline avoids mutable intermediate variables and reads left to right.


Example 2: Block-Based DSL for Configuration

Section titled “Example 2: Block-Based DSL for Configuration”

Problem: Create a simple configuration DSL that uses blocks to build a settings hash.

class Config
def initialize
@settings = {}
end
def database(&block)
@settings[:database] = {}
instance_eval(&block) if block
end
def host(value)
@settings[:database][:host] = value
end
def port(value)
@settings[:database][:port] = value
end
def to_h
@settings.dup
end
end
config = Config.new
config.database do
host "localhost"
port 5432
end
puts config.to_h
# => {:database=>{:host=>"localhost", :port=>5432}}

Explanation: instance_eval runs the block in the context of the Config instance, so host and port calls inside the block invoke the Config methods directly. This pattern is the foundation of Rails configuration DSLs. The block provides a clean, declarative syntax while the class handles the implementation.


Example 3: Error Handling with Custom Exceptions

Section titled “Example 3: Error Handling with Custom Exceptions”

Problem: Define a custom exception hierarchy for an application and handle errors gracefully with retry logic.

class AppError < StandardError; end
class ValidationError < AppError; end
class NotFoundError < AppError; end
def find_user(id)
raise ValidationError, "ID must be positive" unless id.positive?
users = { 1 => "Alice", 2 => "Bob" }
raise NotFoundError, "User #{id} not found" unless users.key?(id)
users[id]
end
def fetch_with_retry(id, max_attempts: 3)
attempts = 0
begin
attempts += 1
find_user(id)
rescue ValidationError => e
puts "Validation failed: #{e.message}"
raise
rescue NotFoundError => e
puts "Attempt #{attempts}: #{e.message}"
retry if attempts < max_attempts
nil
end
end
puts fetch_with_retry(1)
# => "Alice"
puts fetch_with_retry(99)
# => "Attempt 1: User 99 not found"
# => "Attempt 2: User 99 not found"
# => "Attempt 3: User 99 not found"
# => nil

Explanation: Custom exceptions inherit from StandardError (not Exception) to ensure they can be caught by bare rescue. ValidationError is re-raised immediately because it indicates a programming error. NotFoundError is retried because it may be a transient issue. The retry keyword restarts the begin block from the beginning.

  • Variables and Types - Ruby’s dynamic type system and variable scoping rules
  • Methods and Blocks - How blocks, procs, and closures form Ruby’s functional capabilities
  • OOP - Ruby’s object model where everything is an object

Confusing Ruby with Python: Ruby uses end to close blocks, not indentation. Methods are defined with def/end, not def:. Ruby uses @ for instance variables. These syntactic differences cause confusion when switching between languages.

Not using Ruby blocks and procs correctly: Ruby blocks are closures passed to methods. yield calls the block. Procs and Lambdas are first-class block objects. Confusing these three leads to incorrect control flow.

Ignoring Ruby conventions (RuboCop): Ruby has strong community conventions (snake_case for methods, CamelCase for classes, 2-space indentation). RuboCop enforces these automatically. Running rubocop before committing ensures consistent, idiomatic code.