Ruby is built to read like prose: everything is an object, blocks do the looping,
and methods end in ? or ! to tell you what they do. The reference below is
grouped by what you are trying to do, and the filter box searches all of it at
once. Type hash and every hash method on the page comes to you, or type 3.2
to see what arrived in that release.
Every snippet is checked against Ruby 4.0, the current stable release. Anything
that needs a version newer than 2.7 says so in the notes column. Names like user
and nums are placeholders for your own. If you do not have a current Ruby
installed, the official Docker image gets you a shell in one line:
docker run --rm -it ruby:4.0 irb, and the Docker cheat sheet
has the rest. Coming from PHP? The PHP cheat sheet
is grouped the same way, so the two read side by side.
Searches the task, the command and the third column. Press / from anywhere on the page.
242 commands
Running Ruby
| Task | Command |
|---|---|
| Check which version you have | ruby -v |
| Run a script | ruby script.rb |
| Open an interactive shell | irb |
| Run one line without a file | ruby -e 'puts RUBY_VERSION' |
| Check a file for syntax errors | ruby -c file.rb |
| Run with warnings turned on | ruby -w script.rb |
| Run with the YJIT compiler | ruby --yjit script.rb |
| Read the docs for a method | ri Array#map |
Most systems ship an old Ruby or none at all. A version manager such as rbenv, chruby or mise installs the current one next to it without touching the system copy.
Variables and symbols
| Task | Code | Notes |
|---|---|---|
| Print with a newline | puts "Hello" | |
| Print without a newline | print "Hello" | |
| Dump a value while debugging | p value | Prints value.inspect and returns the value |
| Pretty-print a big hash | pp config | |
| Assign a variable | name = "Ada" | No keyword, no type. snake_case by convention |
| Define a constant | MAX_USERS = 100 | Starts with a capital. Reassigning only warns |
| Instance variable | @name = "Ada" | One per object, nil until set |
| Global variable | $stdout | Avoid your own globals |
| A symbol | :active | An immutable name, one object per spelling |
| String to symbol and back | "active".to_sym :active.to_s | |
| Comment | # one line | |
| Is it nil | value.nil? | |
| Call a method only if not nil | user&.address&.city | Returns nil instead of raising |
| Default when nil or false | name = input || "Anonymous" | |
| Assign only if nil or false | @cache ||= {} | |
| Assign several at once | a, b = 1, 2 | |
| Swap two variables | a, b = b, a | |
| First item and the rest | first, *rest = [1, 2, 3] | |
| What class is it | value.class | Everything is an object, even 1 and nil |
| Is it a kind of | value.is_a?(Numeric) | True for subclasses too |
| Same value | a == b | |
| Same value and type | a.eql?(b) | 1 == 1.0 is true, 1.eql?(1.0) is false |
| Same object | a.equal?(b) | |
| Whole-number division | 7 / 2 | Gives 3. Use 7.fdiv(2) or 7 / 2.0 for 3.5 |
| Raise to a power | 2 ** 10 | |
| Readable big numbers | 1_000_000 | |
| String to number, lenient | "42".to_i "3.5".to_f | "abc".to_i is 0 |
| String to number, strict | Integer("42") | Raises ArgumentError on junk |
| Strict, but nil on junk | Integer(input, exception: false) | |
| Make a value read-only | ROLES = %w[admin member].freeze |
Only nil and false are falsy. 0, an empty string and an empty array are all truthy, which trips up anyone arriving from JavaScript, Python or PHP.
Strings
| Task | Code | Notes |
|---|---|---|
| Interpolate | "Hello, #{name}" | Double quotes only. Any expression works |
| No interpolation | 'Hello, #{name}' | Prints #{name} literally |
| Join strings | first + " " + last | |
| Append in place | html << "</ul>" | Changes the string, so not on a frozen one |
| A copy you can change | +'text' str.dup | |
| Length in characters | s.length | "café".length is 4 |
| Length in bytes | s.bytesize | "café".bytesize is 5 |
| Change case | s.upcase s.downcase s.capitalize | |
| Strip whitespace from both ends | s.strip | lstrip and rstrip for one end |
| Remove the trailing newline | gets.chomp | |
| Is it empty | s.empty? | blank? is Rails, not Ruby |
| Does it contain | s.include?("cat") | |
| Does it start or end with | url.start_with?("https") file.end_with?(".rb") | |
| Position of a substring | s.index("cat") | nil when missing |
| Replace the first match | s.sub("cat", "dog") | |
| Replace every match | s.gsub("cat", "dog") | |
| Replace by pattern | s.gsub(/\s+/, " ") | |
| Part of a string | s[0, 5] s[0..4] s[-3..] | Start and length, or a range |
| Split into an array | csv.split(",") | |
| Split into characters | s.chars | |
| Join an array into a string | names.join(", ") | |
| Repeat | "-" * 20 | |
| Pad to a length | n.to_s.rjust(3, "0") | 7 becomes 007. ljust and center too |
| Format with placeholders | format("%s has %d items", name, count) | |
| Format a decimal | "%.2f" % price | |
| Does it match a pattern | s.match?(/\d+/) | Just true or false, and the fastest |
| Capture part of a match | s.match(/(?<year>\d{4})/)[:year] | match returns nil when nothing matches |
| Every match | s.scan(/\d+/) | |
| Reverse | s.reverse | |
| Parse JSON | JSON.parse(text, symbolize_names: true) | require "json" first. Without the option keys are strings |
| Encode to JSON | data.to_json |
Arrays
| Task | Code | Notes |
|---|---|---|
| Create an array | nums = [1, 2, 3] | |
| Array of words | %w[apple banana cherry] | |
| Array of symbols | %i[draft published] | |
| Read by position | nums[0] nums[-1] | Negative counts from the end |
| First or last | nums.first nums.last(2) | |
| Read, raising if out of range | nums.fetch(99) | nums[99] quietly returns nil |
| Append | nums << 4 | push takes several |
| Remove from the end or start | nums.pop nums.shift | |
| Add to the start | nums.unshift(0) | |
| Insert at a position | nums.insert(1, 99) | |
| Remove every copy of a value | nums.delete(3) | |
| Remove at a position | nums.delete_at(0) | |
| How many | nums.length | size is the same. count takes a block |
| Does it contain | nums.include?(3) | |
| Is it empty | nums.empty? | |
| Position of a value | nums.index(3) | nil when missing |
| A slice | nums[1, 2] nums[1..] | |
| Join two arrays | a + b | |
| Union, intersection, difference | a | b a & b a - b | |
| Remove duplicates | nums.uniq | |
| Remove nils | values.compact | |
| Flatten nested arrays | nested.flatten | |
| Sort | nums.sort nums.sort.reverse | |
| Sort by a field | users.sort_by(&:age) | |
| Sort by two fields | users.sort_by { |u| [u.last_name, u.first_name] } | |
| Add up, smallest, largest | nums.sum nums.min nums.max | |
| Item with the largest field | users.max_by(&:score) | min_by too |
| Random item or order | nums.sample nums.shuffle | |
| Pair up two arrays | names.zip(scores) | |
| Split into groups of n | nums.each_slice(2).to_a | |
| A range as an array | (1..10).to_a | 1...10 leaves out the 10 |
| n separate empty arrays | Array.new(3) { [] } | Array.new(3, []) shares one array |
| Change in place | nums.sort! nums.map! { |n| n * 2 } | A trailing ! means it changes the receiver |
Hashes
A hash maps keys to values and remembers insertion order. Symbol keys are the norm in Ruby code, string keys are what you get from JSON and web params.
| Task | Code | Notes |
|---|---|---|
| Create with symbol keys | user = { name: "Ada", age: 36 } | |
| Create with string keys | { "name" => "Ada" } | user[:name] will not find it |
| Build from variables of the same name | { name:, age: } | Ruby 3.1+ |
| Read a value | user[:name] | nil when the key is missing |
| Read, raising if missing | user.fetch(:email) | KeyError names the key |
| Read with a default | user.fetch(:role, "member") | |
| Read a nested value | params.dig(:user, :address, :city) | nil if any level is missing |
| Set a value | user[:role] = "admin" | |
| Remove a key | user.delete(:age) | Returns the removed value |
| Does a key exist | user.key?(:name) | |
| Does a value exist | user.value?("Ada") | |
| Just the keys or values | user.keys user.values | |
| Loop over pairs | user.each { |key, value| puts "#{key}: #{value}" } | |
| Merge, right side wins | defaults.merge(options) | |
| Merge, deciding clashes yourself | a.merge(b) { |_key, old, new| old + new } | |
| Keep some keys | user.slice(:name, :email) | |
| Drop some keys | user.except(:password) | Ruby 3.0+ |
| Keep pairs that pass a test | prices.select { |_, price| price > 10 } | reject for the opposite |
| Change every value | prices.transform_values { |p| p * 2 } | |
| String keys to symbols | data.transform_keys(&:to_sym) | |
| Index a list by a field | users.to_h { |u| [u.id, u] } | |
| Group a list by a field | words.group_by(&:size) | |
| Count how often each value appears | words.tally | |
| Hash where missing keys read as 0 | counts = Hash.new(0) | counts[:x] += 1 then just works |
| Hash where missing keys get their own array | Hash.new { |h, k| h[k] = [] } | |
| Sort by value, largest first | prices.sort_by { |_, v| -v }.to_h | |
| Pair with the largest value | prices.max_by { |_, v| v } | Returns [key, value] |
Blocks and iterators
A block is the chunk of code between braces or do and end that you hand to a method. Braces for one line, do and end for several. Most of these come from Enumerable, so they work on arrays, hashes, ranges and anything else with an each method.
| Task | Code | Notes |
|---|---|---|
| Loop over items | nums.each { |n| puts n } | |
| Loop with the position | names.each_with_index { |name, i| } | |
| Transform every item | nums.map { |n| n * 2 } | |
| Call one method on every item | names.map(&:upcase) | |
| Short block parameter | nums.map { it * 2 } | Ruby 3.4+. _1 before that |
| Keep items that pass a test | nums.select(&:even?) | filter is the same method |
| Drop items that pass a test | nums.reject(&:zero?) | |
| First item that passes a test | users.find { |u| u.admin? } | nil when none do |
| Transform and drop nils in one pass | nums.filter_map { |n| n * 10 if n.even? } | |
| Map, then flatten one level | posts.flat_map(&:tags) | |
| Fold into one value | nums.reduce(0) { |sum, n| sum + n } | nums.reduce(:+) for short |
| Build up a hash or array | words.each_with_object(Hash.new(0)) { |w, counts| counts[w] += 1 } | |
| Split into passing and failing | evens, odds = nums.partition(&:even?) | |
| Do all, any or none pass | nums.all?(&:positive?) nums.any? nums.none? | |
| Count items that pass a test | nums.count(&:even?) | |
| Add up a field | orders.sum(&:total) | |
| Number items from 1 | names.map.with_index(1) { |name, i| "#{i}. #{name}" } | |
| Neighbouring pairs | nums.each_cons(2) { |a, b| } | |
| Repeat n times | 3.times { |i| } | |
| Count up in steps | 0.step(100, 10) { |n| } | |
| Work through an endless sequence | (1..).lazy.select(&:even?).first(3) | Only computes what it needs |
| Hand a block to your own method | def twice = (yield; yield) | |
| Was a block passed | block_given? | |
| Keep a block to call later | def on(event, &handler) | |
| Lambda | square = ->(x) { x * x } | Call with square.(4) or square.call(4) |
| Pass a method as a block | items.each(&method(:puts)) |
Control flow
| Task | Code | Notes |
|---|---|---|
| If, elsif, else | if a ... elsif b ... else ... end | elsif, not else if |
| One-line if | puts "Adult" if age >= 18 | |
| Unless | return unless user | Skip unless with else, it reads backwards |
| Pick one of two values | label = count == 1 ? "item" : "items" | |
| Match a value to a result | case code when 200, 201 then "OK" when 404 then "Not found" else "Error" end | when compares with === |
| Match a range or a class | case age when 0...13 then "child" when 13...18 then "teen" else "adult" end | String, Integer and regexes work too |
| Match on shape | case response in { status: 200, body: } then body end | Pattern matching, Ruby 3.0+ |
| Pull values out of a nested hash | config => { db: { host: } } | Raises if the shape does not match |
| Does it match a pattern | user in { role: "admin" } | true or false |
| While loop | while line = file.gets ... end | |
| Loop until something is true | until queue.empty? ... end | |
| Loop until break | loop do ... break if done ... end | |
| Skip to the next iteration | next if n.odd? | |
| Leave a loop with a value | found = nums.each { |n| break n if n > 2 } | |
| Chain a value through a block | input.then { |s| s.strip } | |
| Both must be true | a && b | and and or have much lower precedence |
Methods
| Task | Code | Notes |
|---|---|---|
| Define a method | def add(a, b) ... end | Returns the last expression |
| One-line method | def add(a, b) = a + b | Ruby 3.0+ |
| Default parameter | def greet(name = "world") | |
| Keyword arguments | def connect(host:, port: 5432) | host is required, port is optional |
| Call with keywords | connect(host: "db", port: 5433) | |
| Any number of arguments | def sum(*nums) | |
| Any keyword arguments | def log(msg, **meta) | |
| Spread an array or hash into arguments | sum(*numbers) connect(**options) | |
| Pass every argument on | def wrapper(...) = target(...) | |
| Pass the block on without naming it | def each(&) = @items.each(&) | Ruby 3.1+. * and ** work the same from 3.2 |
| Return early | return if list.empty? | |
| Return two values | def bounds(list) = [list.min, list.max] | Then low, high = bounds(nums) |
| Method that answers yes or no | def admin? | A trailing ? by convention |
| Dangerous or in-place version | def save! | A trailing ! by convention |
| Does an object have a method | obj.respond_to?(:each) | |
| Call a method by name | obj.public_send(:name) | |
| Parentheses are optional | puts "Hi" puts("Hi") | Keep them when nesting calls |
Classes and modules
| Task | Code | Notes |
|---|---|---|
| Define a class | class User ... end | |
| Constructor | def initialize(name) ... end | Called by User.new |
| Create an object | user = User.new("Ada") | |
| Read-only attribute | attr_reader :name | Generates def name = @name |
| Read and write attribute | attr_accessor :email | attr_writer for write-only |
| Class method | def self.find(id) | Called as User.find(1) |
| Constant on a class | User::ROLES | |
| Inherit from a class | class Admin < User | |
| Call the parent's version | super | Bare super passes the same arguments, super() passes none |
| Methods only the class can call | private | Every def below it is private |
| Group classes under a name | module Billing ... end | Then Billing::Invoice |
| Mix a module's methods into a class | include Comparable | |
| Add a module's methods to the class itself | extend Forwardable | |
| Make objects sortable and comparable | include Comparable def <=>(other) | |
| Make your collection Enumerable | include Enumerable def each | |
| Immutable value object | Point = Data.define(:x, :y) | Ruby 3.2+ |
| Copy with a change | point.with(x: 5) | |
| Quick mutable record | Person = Struct.new(:name, :email) | Keyword arguments work from 3.2 |
| Is it an instance of | user.is_a?(User) | |
| Class and its ancestors | Admin.ancestors | |
| Text for puts and interpolation | def to_s = "#{name} <#{email}>" | |
| Copy an object | user.dup | Shallow copy. clone keeps frozen state |
| Stop changes to an object | user.freeze |
Classes are open: reopening String or Integer and adding a method works anywhere in the program. It is how Rails adds 3.days.ago, and it is also the fastest way to break someone else's code. Prefer a module or a plain method in your own code.
Exceptions
| Task | Code | Notes |
|---|---|---|
| Rescue an error | begin ... rescue ArgumentError => e ... end | |
| Rescue more than one type | rescue Timeout::Error, IOError => e | |
| Run code only if nothing was raised | begin ... rescue ... else ... end | |
| Run code whatever happens | begin ... ensure file.close end | |
| Rescue across a whole method | def load ... rescue JSON::ParserError ... end | No begin needed. Works in do blocks too |
| Raise an error | raise ArgumentError, "Bad id" | |
| Raise a plain RuntimeError | raise "Something broke" | |
| Re-raise the current error | raise | Inside a rescue |
| Define your own error | class PaymentError < StandardError; end | |
| The message and where it came from | e.message e.backtrace.first(5) | |
| Try the begin block again | retry | Count attempts, or it can loop forever |
| Default when anything goes wrong | value = Integer(input) rescue 0 | Hides every StandardError, so use it sparingly |
| Jump out of deep loops | catch(:done) { throw :done, result } | Control flow, not an error |
| Close a file even on error | File.open("log.txt", "a") { |f| f.puts line } | The block form closes it for you |
A bare rescue catches StandardError and its subclasses, which is almost always what you want. Rescuing Exception also catches Interrupt and SystemExit, so Ctrl+C and exit stop working.
Gems and Bundler
A gem is a Ruby package. Bundler reads the Gemfile, writes the exact versions it resolved to Gemfile.lock, and makes sure the app loads those versions and no others.
| Task | Command |
|---|---|
| Install a gem globally | gem install rails |
| List installed gems | gem list |
| Remove a gem | gem uninstall rails |
| Start a Gemfile | bundle init |
| Add a gem to the Gemfile and install it | bundle add rack |
| Add with a version constraint | bundle add rack --version "~> 3.1" |
| Add to a group | bundle add rspec --group test |
| Remove a gem from the Gemfile | bundle remove rspec |
| Install what the Gemfile.lock says | bundle install |
| Skip a group on this machine | bundle config set --local without test |
| Upgrade one gem within its constraint | bundle update rack |
| See which gems have newer versions | bundle outdated --only-explicit |
| Run a command with the bundled versions | bundle exec rspec |
| Where a gem is and which version | bundle info rack |
| Check everything is installed | bundle check |
| Add a platform for Linux servers | bundle lock --add-platform x86_64-linux |
| Load the bundle in plain Ruby | require "bundler/setup" |
bundle install and bundle update are not interchangeable. install reproduces Gemfile.lock, so it is what you run after cloning and in CI. update resolves newer versions and rewrites the lock file, so it is a change you commit and review.
A class, start to finish
Most of the class syntax above in one place: a module as a namespace, keyword
arguments, attr_reader, Comparable, a class method, a private helper and a
custom error.
module Billing
class InvalidAmount < StandardError; end
class Money
include Comparable
SYMBOLS = { gbp: "£", usd: "$" }.freeze
attr_reader :pence, :currency
def initialize(pence, currency: :gbp)
raise InvalidAmount, "pence must be an Integer" unless pence.is_a?(Integer)
@pence = pence
@currency = currency
end
def self.from_pounds(pounds, **options)
new((pounds * 100).round, **options)
end
def +(other)
check_currency!(other)
Money.new(pence + other.pence, currency:)
end
def <=>(other)
check_currency!(other)
pence <=> other.pence
end
def to_s = format("%s%.2f", SYMBOLS.fetch(currency), pence / 100.0)
private
def check_currency!(other)
return if other.currency == currency
raise ArgumentError, "cannot mix #{currency} and #{other.currency}"
end
end
end
price = Billing::Money.new(1999)
puts price + Billing::Money.from_pounds(5) # £24.99
puts [price, Billing::Money.new(500)].max # £19.99Defining <=> and including Comparable gives you <, >, between?, clamp,
and lets max, min and sort work on a list of Money. Money is held in pence
as an integer on purpose: floats cannot represent most decimal fractions exactly,
so 0.1 + 0.2 == 0.3 is false in Ruby, as it is in almost every language.
Blocks, procs and lambdas
A method can call the block it was given with yield, or capture it as an object
with &. Procs and lambdas are both block objects, but they disagree about
arguments and return.
def with_timing(label)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = yield
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
puts format("%s took %.3fs", label, elapsed)
result
end
total = with_timing("sum") { (1..1_000_000).sum }
# A lambda checks its arguments, like a method does.
add = ->(a, b) { a + b }
add.(1, 2) # 3
add.(1) # ArgumentError
# A proc fills gaps with nil and ignores extras.
loose = proc { |a, b| [a, b] }
loose.call(1) # [1, nil]
loose.call(1, 2, 3) # [1, 2]
# Any object with to_proc can stand in for a block.
%w[ada grace].map(&:capitalize) # ["Ada", "Grace"]Pattern matching
case with in matches on the shape of a value rather than its equality, and
binds the parts you name as local variables. It is at its best on parsed JSON.
require "json"
response = JSON.parse(body, symbolize_names: true)
message =
case response
in { status: 200, data: { user: { name: String => name } } }
"Welcome back, #{name}"
in { status: 404 }
"No such user"
in { status: 500.. => status, error: }
"Server error #{status}: #{error}"
in { status: }
"Unexpected status #{status}"
endHash patterns match when the listed keys are present and ignore any others. Array
patterns must match the whole array unless you add *rest. If no branch matches
and there is no else, Ruby raises NoMatchingPatternError, which is usually
what you want: a response you did not plan for fails loudly.
Data and Struct
Data.define makes a small immutable value class with a constructor, equality,
to_h and pattern matching support. Struct is the older, mutable sibling.
Point = Data.define(:x, :y) do
def +(other) = with(x: x + other.x, y: y + other.y)
def to_s = "(#{x}, #{y})"
end
a = Point.new(x: 1, y: 2)
b = Point.new(3, 4) # positional arguments work too
puts a + b # (4, 6)
a == Point.new(x: 1, y: 2) # true, compared by value
a.to_h # {x: 1, y: 2}
case a
in { x: 0 } then "on the y axis"
in Point(x:, y:) then "at #{x}, #{y}"
end
Person = Struct.new(:name, :email)
ada = Person.new(name: "Ada")
ada.email = "ada@example.com" # Struct members can be changedStrings across several lines
The squiggly heredoc, <<~, strips the indentation of its least indented line, so
the text can sit at the same depth as the code around it. It interpolates like a
double-quoted string. Put the marker in single quotes to turn interpolation off.
def welcome_email(user)
<<~TEXT
Hi #{user.name},
Thanks for signing up. Your username is #{user.username}.
TEXT
end
query = <<~'SQL'
SELECT * FROM users WHERE name LIKE '#{not interpolated}'
SQLA Gemfile, start to finish
bundle add writes these lines for you, but it helps to read them. ~> 3.1
means at least 3.1 and below 4.0, so minor updates come in and major ones do not.
# Gemfile
source "https://rubygems.org"
ruby "~> 4.0"
gem "rack", "~> 3.1"
gem "puma", "~> 8.0"
group :development, :test do
gem "rspec", "~> 3.13"
endCommit Gemfile and Gemfile.lock together, and run your tools through
bundle exec so they use the locked versions. The
Git cheat sheet covers the rest of that workflow.
For a one-file script, bundler/inline installs and loads gems without a
separate Gemfile:
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "rack", "~> 3.1"
end
puts Rack.releaseGotchas
The mistakes that turn up in almost every Ruby codebase at some point.
| Looks right | What actually happens | Do this instead |
|---|---|---|
if count when count is 0 | The branch runs: only nil and false are falsy | if count.positive?, or count.zero? |
Array.new(3, []) | All three slots are the same array | Array.new(3) { [] } |
Hash.new([]) then h[:a] << 1 | Every key shares one default array, and h stays empty | Hash.new { |h, k| h[k] = [] } |
params[:name] on parsed JSON | nil, because JSON keys are strings | params["name"], or parse with symbolize_names: true |
7 / 2 | 3, integer division | 7.fdiv(2) or 7 / 2.0 |
users.sort_by(&:age) for a stable order | Ties can come out in any order | users.sort_by.with_index { |u, i| [u.age, i] } |
rescue Exception | Also catches Ctrl+C and exit | rescue => e, or a specific class |
ok = valid? and saved? | ok gets valid? alone, because = binds tighter than and | ok = valid? && saved? |
"abc".to_i | 0, no error | Integer("abc"), which raises |
name << "!" on a literal | FrozenError under frozen_string_literal: true | name + "!", or start from +"" |
Ruby's sort is a quick sort
variant, which is why equal items are not guaranteed to keep their order. A stable
algorithm such as merge sort keeps
them. Both are on the site as step-through visualisations.
Common questions
Which version of Ruby does this cheat sheet cover?
Ruby 4.0, the current stable release, which came out in December 2025. Almost everything on the page also runs on Ruby 3, and anything newer than 2.7 says so in the notes column, for example the it block parameter needs 3.4, Data.define needs 3.2 and hash shorthand needs 3.1. Run ruby -v to see which version you have.
What is the difference between a symbol and a string in Ruby?
A symbol, like :name, is an immutable identifier, and every use of :name is the same object. A string, like "name", is text you can change, and each literal can be a separate object. Use symbols for names the program uses internally, such as hash keys and method names, and strings for text that comes from or goes to people, files and the network.
What is the difference between a proc and a lambda?
Both are blocks turned into objects. A lambda checks its argument count and raises if it is wrong, and return inside it returns from the lambda. A proc ignores missing or extra arguments, and return inside it returns from the method the proc was defined in. Lambdas behave like small methods, so reach for them unless you specifically want proc behaviour.
What is the difference between puts, print and p?
puts prints a value's to_s followed by a newline, and prints each element of an array on its own line. print does the same without the newline. p prints the inspect version, so strings keep their quotes and nil shows as nil, and it returns the value, which makes it handy for debugging in the middle of an expression. All three are methods on Kernel.
What does the ! and ? at the end of a Ruby method mean?
Nothing to the interpreter, a lot to readers. A trailing question mark marks a method that answers yes or no, like empty? or admin?. A trailing exclamation mark marks the more dangerous of a pair: sort returns a new array and sort! changes the one it is called on, save returns false on failure and save! raises.
What is the difference between include and extend?
include adds a module's methods to instances of the class, so every object gets them. extend adds them to the one object you call it on, and inside a class body that object is the class, so they become class methods. Enumerable and Comparable are meant to be included, Forwardable is meant to be extended.
Should I commit Gemfile.lock?
Yes, for an application. The lock file records the exact version of every gem, so bundle install gives every developer, CI run and server the same code. For a gem you publish it is optional: the apps that install your gem ignore your lock file and resolve their own versions, though committing it keeps your own CI reproducible.
Why does my string raise FrozenError?
Something froze it. A file that starts with the comment frozen_string_literal: true makes every string literal in it immutable, and many projects turn that on everywhere. Ruby 3.4 and 4.0 go further towards frozen literals and warn when you change one, with deprecation warnings enabled. Build the string with +"" or call dup to get a copy you can change.
