Ruby 3.x lineage brings pattern matching, fibers / scheduler-based concurrency, and YJIT (just-in-time compiler — enable with
RUBY_YJIT_ENABLE=1 or --yjit). Type information is opt-in via
RBS signatures or Sorbet. Modern web stacks: Rails 7 + Hotwire for full-stack,
Sinatra / Roda for micro. Background work runs on Sidekiq + async-gem fibers.
Install · bundlerSetup
bash
# Install via rbenv (recommended) or asdf
brew install rbenv ruby-build
rbenv install 3.3.0
rbenv global 3.3.0
# Verify
ruby -v # ruby 3.3.0 ...
gem -v && bundler -v
# New project
mkdir myapp && cd myapp
bundle init # creates Gemfile
bundle add sinatra puma # add deps
bundle install # install + write Gemfile.lock
# Daily commands
ruby script.rb # run a file
bundle exec rspec # run tests in the bundle's context
irb -r ./lib/myapp # REPL with your code loaded
bundle exec rubocop -A # auto-fix lints
gem install bundler # update bundler itself
Where things liveStandard library map
require "json"
JSON.parse, JSON.generate.
require "csv"
RFC-4180 CSV reader / writer.
require "net/http"
Built-in HTTP client. Verbose; most prod uses a gem.
require "uri"
Parse / build URIs.
require "set"
Set class.
require "ostruct"
Quick attribute-bag struct.
require "fileutils"
rm_rf, mkdir_p, cp, mv.
require "open3"
Run shell commands with stdin / stdout / stderr / status.
require "logger"
Stdlib structured-ish logger.
require "securerandom"
UUIDs, tokens, random bytes.
require "date" / "time"
Date arithmetic + extended Time parsing.
require_relative "./foo"
Load a sibling file. Skips load-path resolution.
Bindings · control flowSyntax basics
x = 5
Local variable. Snake_case.
@instance / @@class / $global
Sigils distinguish scope.
MAX = 100
Constant. UpperCamelCase. Mutable but conventionally not.
unless cond / until cond
Negative forms. unless avoids if !.
x += 1 if x > 0
Statement modifier — the trailing-if idiom.
a, b = 1, 2
Parallel assignment.
a, *rest = [1, 2, 3, 4]
Splat destructuring.
case x; when 1..10 then … end
Case/when. Uses ===.
case x in {name:, age: 18..} then … end
Pattern matching (Ruby 3+).
begin … rescue => e … ensure … end
Exception handling.
def name(*args, **kw, &blk)
Splat args, double-splat kwargs, block.
def square(x) = x * x
Endless method (Ruby 3+).
Text manipulationStrings & symbols
"hello, #{name}"
Double-quoted = interpolation + escapes.
'no interp'
Single-quoted = literal.
<<~SQL … SQL
Indented heredoc. Strips common leading whitespace.
s.upcase / downcase / capitalize / swapcase
Common case ops. !-versions mutate.
s.strip / chomp / chop
Trim whitespace / trailing newline / last char.
s.split(",") / s.split(/\s+/)
String or regex separator.
s.gsub(/\d+/, "X")
Global substitute. sub = first only.
s.match?(/^\d+$/) / s =~ /pat/ / s.scan(/\w+/)
Test / first match / all matches.
s.tr("aeiou", "*")
Char-by-char translate.
format("%5.2f %s", pi, name) / "%s: %d" % [k, v]
printf-style formatting.
:name
Symbol. Interned immutable identifier.
"hello".to_sym / :hello.to_s
Convert string ↔ symbol.
Array · Hash · Range · SetCollections
Arrays
xs = [1, 2, 3] / Array.new(3) { |i| i * 2 }
Literal / builder.
%w[a b c] / %i[a b c]
Array of strings / symbols.
xs << 4 / xs.push(4) / xs.unshift(0)
Append / push end / push front.
xs.first / last / first(3) / last(3)
Edge access.
xs.compact / xs.flatten / xs.uniq
Drop nils / flatten / dedupe.
xs.zip(ys, zs)
Pair up rows across arrays.
xs.partition { |x| x > 0 }
Split into matching / non-matching.
xs.each_slice(2) / xs.each_cons(2)
Non-overlap chunks / sliding window.
Hashes
h = { name: "Ada", age: 36 }
Symbol-key shorthand.
h[:name] / h.fetch(:name, "anon")
Index / index-with-default-or-block.
h.merge(other) / h.merge!(other) { |k, a, b| … }
Combine with conflict resolution.
h.transform_values { |v| v.upcase }
Map over values only.
h.group_by { |k, v| v.class }
Bucket entries.
h.slice(:a, :b) / h.except(:a)
Sub-hashes.
h.dig(:a, :b, :c)
Safe deep access. Returns nil if any link missing.
h.to_a / h.keys / h.values
Convert / iterate.
Range & Set
(1..10) / (1...10)
Inclusive / exclusive range.
(1..).step(2).first(5)
Endless ranges + lazy ops.
Set[1, 2, 3] (require "set")
Hash-backed set.
(a & b), (a | b), (a - b), (a ^ b)
Set operators — on arrays too.
The killer featureBlocks, procs, lambdas
method { |x| … } / method do |x|; … end
Curly = inline; do/end = multi-line.
yield
Invoke the implicit block from inside a method.
def f(&blk); blk.call(…); end
Capture block as a Proc.
block_given?
Check whether a block was passed.
Proc.new { |x| … }
Proc. Loose arity. return exits enclosing method.
->(x, y) { x + y }
Lambda. Strict arity. return exits the lambda only.
fn.call(x) / fn.(x) / fn[x]
Three equivalent invocations.
xs.map(&:upcase)
Symbol-to-proc. Same as xs.map { |s| s.upcase }.
method(:foo).to_proc / xs.each(&method(:puts))
Pass a method as a block.
Worked example
ruby
# Blocks are the heart of Ruby — every method can take one
# yield invokes the block; block_given? checks
def each_word(text)
text.split.each { |w| yield w if block_given? }
end
each_word("hello world from ruby") { |w| puts w.upcase }
# Convert block to a Proc with &
def benchmark(&blk)
t0 = Time.now
blk.call
puts "elapsed: #{(Time.now - t0).round(3)}s"
end
benchmark { (1..1_000_000).inject(:+) }
# Procs vs lambdas — lambdas check arity and return locally
square = ->(x) { x * x }
puts square.call(5) # 25
puts square.(5) # alt call syntax
puts square[5] # alt call syntax
# Symbol-to-proc — pass a method by name
%w[hello world].map(&:upcase).each(&method(:puts))
# Yielding with multiple values
def each_pair(h)
h.each { |k, v| yield k, v }
end
each_pair(a: 1, b: 2) { |k, v| puts "#{k} → #{v}" }
map · select · reduceEnumerable
xs.each / each_with_index / each_with_object({})
Iterate. each_with_object threads state.
xs.map { |x| x * 2 } / xs.collect
Transform. collect is alias.
xs.flat_map { |x| [x, x] }
Map + concat.
xs.select { … } / reject { … }
Keep / drop matches.
xs.find { … } / detect
First match.
xs.count { … }
Count matching.
xs.reduce(:+) / inject(0) { |s, x| s + x }
Fold. Pass an operator symbol or a block.
xs.group_by { |x| x.role }
Bucket. Returns Hash.
xs.tally / xs.tally_by { … }
Frequency count.
xs.min_by { … } / max_by / sort_by
"By" variants take a key function.
xs.lazy.select { … }.first(5)
Lazy chains for large / infinite enumerables.
xs.any? / all? / none? / one?
Predicate quantifiers.
OOP · mixinsClasses & modules
class User; end
Class. Reopened on subsequent declarations.
class Admin < User; end
Single inheritance.
attr_reader / attr_writer / attr_accessor :name
Generate getter / setter / both.
def initialize(…); end
Constructor.
module M; def hello; "hi"; end; end
Module. No instances; for mixins or namespacing.
include M / extend M
Mix into instances / mix into the class itself.
prepend M
Mix in above the class — module methods win.
Comparable + def <=>
Define spaceship; gain ==, <, between?, etc.
Struct.new(:a, :b) do def total = a + b; end
Quick value-type class. Equality + hash for free.
Data.define(:a, :b)
Immutable Struct (Ruby 3.2+).
class << self / self.method
Class-level methods.
private / protected / public
Visibility. Apply downward inside class body.
private def foo
Preferred Per-method visibility (modern style).
Worked example
ruby
# Modules as mixins — composition over inheritance
module Greetable
def greet
"hello, #{name}"
end
end
# Comparable + <=> gives you ==, <, <=, >, >=, between?
class User
include Greetable
include Comparable
attr_accessor :name, :score # read + write accessors
def initialize(name, score)
@name, @score = name, score
end
def <=>(other) # define one operator → six come free
score <=> other.score
end
def to_s = ""
end
ada = User.new("Ada", 42)
grace = User.new("Grace", 99)
puts ada.greet
puts [ada, grace].max # uses <=>
puts ada.between?(grace, ada) # false
# Pattern matching (Ruby 3+)
case ada
in { name:, score: } if score >= 50
puts "#{name} qualifies"
in User(name:, score:)
puts "#{name} below threshold (#{score})"
end
The async gem ships fiber-based scheduling. Three concurrent GitHub calls, JSON parsed, per-call error isolation.
ruby
# Gemfile:
# source "https://rubygems.org"
# gem "async-http"
# gem "async"
require "async"
require "async/http/internet"
require "async/barrier"
require "json"
OWNERS = %w[ruby sinatra rails]
Async do
internet = Async::HTTP::Internet.new
barrier = Async::Barrier.new
results = {}
OWNERS.each do |owner|
barrier.async do
url = "https://api.github.com/users/#{owner}/repos?per_page=5"
resp = internet.get(url, [["User-Agent", "devshelf"]])
data = JSON.parse(resp.read)
results[owner] = data.first(5).map { |r| [r["name"], r["stargazers_count"]] }
rescue => e
results[owner] = e
end
end
barrier.wait
results.each do |owner, data|
if data.is_a?(Exception)
warn "#{owner}: #{data.message}"
else
data.each { |name, stars| puts "#{owner}/#{name} ★#{stars}" }
end
end
ensure
internet&.close
end
Best practiceGood to know
Prefer modules + composition over deep class trees.
Ruby’s strength is mix-ins — include Comparable for free comparisons, include Enumerable for the entire collection API after defining each.
Bang (!) methods mutate.
Convention: map returns a new array, map! mutates in place. Stick to this when writing your own; readers expect it.
Enable YJIT in production.
Set RUBY_YJIT_ENABLE=1 (or pass --yjit). 15–30% speedup on Rails workloads with no code changes.
Common trapsWatch out for
nil and false are the only falsy values.
Everything else, including 0 and empty strings, is truthy. Coming from Python or JS this catches everyone.
String mutation is in-place.s.upcase! changes the same object every reference points at — including hash keys, which then can’t be found anymore. Use String#freeze or the non-bang version.
return inside a Proc exits the enclosing method.
That’s the LocalJumpError trap. Use a lambda when you want to return locally, or use next to exit the block.
Ruby is a dynamic, object-oriented scripting language known for its clean, expressive syntax. It is most widely used for web development with Rails and Sinatra, scripting and automation, CLI tools, and data processing. Ruby 3.3+ with YJIT delivers significantly faster performance than older versions.
What are Ruby blocks, procs, and lambdas?
A block is an anonymous chunk of code passed to a method with do...end or { }. A proc is a block saved in a variable with Proc.new or proc { }; it does not enforce argument count and returns from the enclosing method. A lambda is stricter — it checks argument count and returns from itself.
What is Enumerable in Ruby?
Enumerable is a mixin module included in Array, Hash, Range, and other collections. It provides map, select, reject, reduce, sort_by, group_by, flat_map, each_with_object, and dozens more. Any class that defines each and includes Enumerable gets all these methods automatically.
What is Ruby pattern matching?
Ruby 3.x added first-class pattern matching with the case/in syntax. Patterns can match values, arrays (in [a, b, *rest]), hashes (in { name: String => n }), find patterns (in [*, 0, *]), and pinned variables (in ^x). Use the one-line form expr => pattern for destructuring assignment.
Is Ruby still used in 2026?
Yes. Ruby remains the language of choice for Rails-based startups and established SaaS platforms. The ecosystem is mature: Rails 7, Hotwire, and Turbo handle full-stack web; Sidekiq powers background jobs; YJIT in Ruby 3.3+ closed much of the performance gap with compiled languages.