Ruby interviews test language fundamentals, object-oriented design, metaprogramming, and Rails-adjacent patterns. This guide covers 50 of the most common questions — with concise answers and runnable code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| OOP & objects | Everything is an object, class vs module, extend/include |
| Blocks, procs, lambdas | Difference, yield, &, -> syntax |
| Metaprogramming | method_missing, define_method, send, open classes |
| Modules & mixins | include vs extend vs prepend |
| Symbols & strings | :symbol vs "string", immutability, memory |
| Iterators & enumerables | each, map, select, reduce, lazy enumerators |
| Exceptions | rescue, ensure, raise, custom errors |
| Concurrency | GIL, threads, fibers, Ractors (Ruby 3) |
Basics
1. What is Ruby and what makes it unique?
Ruby is a dynamic, object-oriented, interpreted language created by Yukihiro Matsumoto in 1995. Key traits:
- Everything is an object — integers,
nil,true, classes themselves - Principle of Least Surprise — behaviour matches programmer intuition
- Open classes — any class can be reopened and modified at runtime
- Blocks, procs, lambdas — first-class closures baked into the language
- Expressive syntax — reads like natural language
# Everything is an object
1.class # => Integer
nil.class # => NilClass
true.respond_to?(:class) # => true
# Integers have methods
3.times { print "Hi " } # Hi Hi Hi
-5.abs # => 5
2. What is the difference between nil, false, and falsy values in Ruby?
In Ruby only nil and false are falsy. Everything else — 0, "", [], {} — is truthy.
puts "falsy" if nil # falsy
puts "falsy" if false # falsy
puts "truthy" if 0 # truthy ← different from Python/JS
puts "truthy" if "" # truthy
puts "truthy" if [] # truthy
nil means "no value"; false means "boolean false". Use nil? to check for nil explicitly.
3. Explain ==, equal?, eql?, and === in Ruby.
| Method | Checks |
|---|---|
== |
Value equality (overrideable) |
equal? |
Object identity (same object_id) |
eql? |
Value equality with type check (used in Hash keys) |
=== |
Case equality (used by case/when) |
a = "hello"
b = "hello"
a == b # => true (same value)
a.equal?(b) # => false (different objects)
a.eql?(b) # => true (same value + type)
(1..10) === 5 # => true (range includes 5)
/\d+/ === "42" # => true (regex match)
String === "hi" # => true (is-a check)
4. What are Ruby symbols and how do they differ from strings?
Symbols are immutable, memory-efficient identifiers prefixed with :.
:name.class # => Symbol
"name".class # => String
:name.object_id == :name.object_id # => true (same object every time)
"name".object_id == "name".object_id # => false (new object each time)
| Symbol | String | |
|---|---|---|
| Mutable | No | Yes |
| Memory | One instance ever | New object each literal |
| Use case | Hash keys, method names, identifiers | Text data, manipulation |
Use symbols for hash keys and method options; strings for text content.
# Prefer symbol keys in hashes
person = { name: "Alice", age: 30 } # { :name => "Alice", :age => 30 }
5. What is the difference between puts, print, and p?
puts "hello" # prints "hello\n" — calls .to_s, adds newline
print "hello" # prints "hello" — no newline
p "hello" # prints "\"hello\"\n" — calls .inspect, useful for debugging
p [1, nil, ""] # => [1, nil, ""] shows nil and empty string clearly
p returns the object; puts/print return nil. Use p for debugging.
6. What are freeze and frozen??
freeze makes an object immutable. frozen? checks if it is.
str = "hello"
str.freeze
str << " world" # RuntimeError: can't modify frozen String
str.frozen? # => true
# Ruby 3+ string literals can be globally frozen with:
# frozen_string_literal: true
Symbols and integers are frozen by default. Use freeze for constants you never want mutated.
Blocks, Procs, and Lambdas
7. What is a block in Ruby?
A block is anonymous code passed to a method, delimited by do...end or { }.
[1, 2, 3].each do |n|
puts n * 2
end
[1, 2, 3].each { |n| puts n * 2 }
Blocks are not objects — you can't assign them to variables directly. Use yield to call the block inside a method.
8. What is yield and how does it work?
yield transfers control from the method to the block passed to it.
def greet
puts "Before"
yield "World" if block_given?
puts "After"
end
greet { |name| puts "Hello, #{name}!" }
# Before
# Hello, World!
# After
block_given? safely checks whether a block was provided.
9. What is the difference between a Proc and a Lambda?
Both are callable objects (closures), but they differ in two key ways:
| Proc | Lambda | |
|---|---|---|
| Created with | Proc.new { } or proc { } |
lambda { } or ->() { } |
| Return behaviour | return exits the enclosing method |
return exits only the lambda |
| Arity check | Lenient (extra args = nil, fewer = ok) | Strict (wrong arg count raises ArgumentError) |
# Return difference
def test_proc
p = Proc.new { return "from proc" }
p.call
"after proc" # never reached
end
def test_lambda
l = lambda { return "from lambda" }
l.call
"after lambda" # this IS reached
end
test_proc # => "from proc"
test_lambda # => "after lambda"
# Arity difference
p = Proc.new { |a, b| [a, b] }
p.call(1) # => [1, nil] (no error)
l = lambda { |a, b| [a, b] }
l.call(1) # ArgumentError: wrong number of arguments
10. What does the & (ampersand) operator do?
& converts between blocks and procs:
# Convert proc to block when calling a method
double = proc { |n| n * 2 }
[1, 2, 3].map(&double) # => [2, 4, 6]
# Convert symbol to proc (shorthand)
[1, 2, 3].map(&:to_s) # => ["1", "2", "3"]
# equivalent to: [1,2,3].map { |n| n.to_s }
# Capture block as proc in method definition
def capture(&block)
block.call(10)
end
capture { |n| n * 3 } # => 30
11. What is a closure?
A closure is a function that captures the environment (variables) where it was defined.
def make_counter
count = 0
increment = -> { count += 1; count }
decrement = -> { count -= 1; count }
[increment, decrement]
end
inc, dec = make_counter
inc.call # => 1
inc.call # => 2
dec.call # => 1
The lambda captures count from make_counter's scope and persists it.
Object-Oriented Programming
12. How does Ruby's object model work?
Every object has a class, every class is an object (instance of Class), and Class itself inherits from Module which inherits from Object.
1.class # => Integer
Integer.class # => Class
Class.class # => Class
Class.superclass # => Module
Module.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil
Method lookup walks: object's singleton class → class → superclass chain → Kernel → BasicObject.
13. What is the difference between include, extend, and prepend?
All three add a module's methods to a class, but differently:
| Method | Adds module methods as | Target |
|---|---|---|
include |
Instance methods | class instances |
extend |
Class-level (singleton) methods | the class itself |
prepend |
Instance methods (before class in lookup) | class instances |
module Greetable
def hello
"Hello from #{self.class}"
end
end
class Person
include Greetable
end
Person.new.hello # => "Hello from Person"
class Robot
extend Greetable
end
Robot.hello # => "Hello from Robot" (class method)
module Logging
def hello
puts "Calling hello"
super
end
end
class Cat
prepend Logging
def hello
"Meow"
end
end
Cat.new.hello # "Calling hello" then "Meow"
14. What is a singleton method?
A method defined on a specific object instance, not its class.
dog = Object.new
def dog.speak
"Woof!"
end
dog.speak # => "Woof!"
# Other Object instances don't have this method
Class methods are singleton methods on the class object:
class Cat
def self.species # self is the Cat class object
"Felis catus"
end
end
Cat.species # => "Felis catus"
15. What are attr_reader, attr_writer, and attr_accessor?
Macros that auto-generate getter/setter instance methods:
class Person
attr_reader :name # generates: def name; @name; end
attr_writer :age # generates: def age=(v); @age = v; end
attr_accessor :email # generates both reader and writer
def initialize(name, age, email)
@name = name
@age = age
@email = email
end
end
p = Person.new("Alice", 30, "alice@example.com")
p.name # => "Alice"
p.age = 31 # sets @age
p.email # => "alice@example.com"
p.email = "new@example.com"
16. What is the difference between public, protected, and private?
| Visibility | Accessible from | Can be called with explicit receiver? |
|---|---|---|
public |
Anywhere | Yes |
protected |
Inside class and subclasses | Yes (on self or same-class instance) |
private |
Inside class only | No (implicit self only) |
class BankAccount
def balance_greater_than?(other)
balance > other.balance # protected method, called on another instance
end
protected
def balance
@balance
end
private
def secret_pin
1234
end
end
17. What is self in Ruby?
self refers to the current object in context:
class Dog
def self.kingdom # self = Dog class
"Animalia"
end
def name # self = instance
self.class.name
end
def bark
puts self.inspect # self = current Dog instance
end
end
Inside instance methods, self is the receiver. Inside class methods (and class << self blocks), self is the class.
Modules and Mixins
18. What is a module in Ruby and when do you use it?
Modules serve two purposes:
- Namespace — group related constants/methods, avoid name clashes
- Mixin — share behaviour across unrelated classes (Ruby's answer to multiple inheritance)
# Namespace
module Payments
class Invoice; end
class Receipt; end
end
Payments::Invoice.new
# Mixin
module Serializable
def to_json
instance_variables.each_with_object({}) do |var, hash|
hash[var.to_s.delete("@")] = instance_variable_get(var)
end.to_json
end
end
class User
include Serializable
end
19. What is the method lookup path (MRO)?
Ruby uses a linear lookup chain you can inspect with .ancestors:
module A; end
module B; end
class C
include A
include B
end
C.ancestors # => [C, B, A, Object, Kernel, BasicObject]
prepend inserts the module before the class in the chain.
Metaprogramming
20. What is method_missing and when should you use it?
method_missing is called when Ruby can't find a method. Override it to intercept undefined calls.
class DynamicClass
def method_missing(name, *args)
if name.to_s.start_with?("say_")
word = name.to_s.sub("say_", "")
puts word
else
super # important: delegate unknown methods up
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("say_") || super
end
end
obj = DynamicClass.new
obj.say_hello # "hello"
obj.say_world # "world"
Always define respond_to_missing? alongside method_missing. Use sparingly — it's slow and hides errors.
21. What is define_method?
define_method creates methods dynamically at runtime:
class Report
["html", "pdf", "csv"].each do |format|
define_method("export_#{format}") do
"Exporting as #{format.upcase}"
end
end
end
r = Report.new
r.export_html # => "Exporting as HTML"
r.export_pdf # => "Exporting as PDF"
Prefer define_method over eval for dynamic method creation — it's safer and scoped correctly.
22. What does send do?
send calls a method by name (string or symbol), bypassing visibility:
"hello".send(:upcase) # => "HELLO"
"hello".send("length") # => 5
class Secret
private
def hidden; "shhh"; end
end
Secret.new.send(:hidden) # => "shhh" (bypasses private)
Secret.new.public_send(:hidden) # NoMethodError (respects private)
Use public_send when you don't intentionally need to bypass visibility.
23. What are open classes (monkey patching)?
Ruby allows reopening any class — including built-ins — to add or override methods.
class Integer
def factorial
return 1 if self <= 1
self * (self - 1).factorial
end
end
5.factorial # => 120
Powerful but risky — can break third-party code. Prefer refinements (scoped monkey patches) in Ruby 2+:
module FactorialRefinement
refine Integer do
def factorial
return 1 if self <= 1
self * (self - 1).factorial
end
end
end
using FactorialRefinement
5.factorial # => 120 (only in this file/scope)
24. What is eval, instance_eval, and class_eval?
| Method | Context (self) |
Use |
|---|---|---|
eval(str) |
Current context | Execute string as Ruby — avoid |
obj.instance_eval |
The object | Access instance variables |
Klass.class_eval |
The class | Define methods dynamically |
class Person
def initialize(name); @name = name; end
end
alice = Person.new("Alice")
alice.instance_eval { @name } # => "Alice"
Person.class_eval do
def greet
"Hi, I'm #{@name}"
end
end
alice.greet # => "Hi, I'm Alice"
Iterators and Enumerables
25. What is the Enumerable module?
Enumerable provides collection iteration methods to any class that defines each:
class NumberBag
include Enumerable
def initialize(*nums)
@nums = nums
end
def each(&block)
@nums.each(&block)
end
end
bag = NumberBag.new(3, 1, 4, 1, 5, 9)
bag.sort # => [1, 1, 3, 4, 5, 9]
bag.max # => 9
bag.select(&:odd?) # => [3, 1, 1, 5, 9]
bag.map { |n| n * 2 } # => [6, 2, 8, 2, 10, 18]
26. What is the difference between map, select, reject, and reduce?
nums = [1, 2, 3, 4, 5]
nums.map { |n| n * 2 } # => [2, 4, 6, 8, 10] transform each
nums.select { |n| n.even? } # => [2, 4] keep matching
nums.reject { |n| n.even? } # => [1, 3, 5] drop matching
nums.reduce(0) { |sum, n| sum + n } # => 15 fold to value
# reduce shorthand
nums.reduce(:+) # => 15
nums.reduce(:*) # => 120
inject is an alias for reduce.
27. What is each_with_object vs inject/reduce?
# inject — accumulator is return value of block
[1,2,3].inject([]) { |acc, n| acc << n * 2; acc } # note: must return acc
# each_with_object — accumulator is always the passed object
[1,2,3].each_with_object([]) { |n, acc| acc << n * 2 }
# => [2, 4, 6] (cleaner — no need to return acc)
# Building a hash
[:a, :b].each_with_index.each_with_object({}) do |(el, i), hash|
hash[el] = i
end
# => { a: 0, b: 1 }
28. What is a lazy enumerator?
lazy defers computation until values are needed — useful for infinite sequences:
# Without lazy — would loop forever
(1..Float::INFINITY).select(&:odd?).first(5) # hangs!
# With lazy — stops after finding 5
(1..Float::INFINITY).lazy.select(&:odd?).first(5)
# => [1, 3, 5, 7, 9]
# Chain multiple operations without creating intermediate arrays
(1..Float::INFINITY).lazy
.map { |n| n ** 2 }
.select { |n| n % 3 == 0 }
.first(3)
# => [9, 36, 81]
Strings and Data Structures
29. What are the main ways to create strings in Ruby?
# Double quotes — interpolation + escape sequences
name = "Alice"
"Hello, #{name}!" # => "Hello, Alice!"
"Line\nBreak" # \n is interpreted
# Single quotes — literal, no interpolation
'Hello, #{name}!' # => "Hello, \#{name}!" (literal)
'Line\nBreak' # \n is NOT interpreted (except \\ and \')
# Heredoc
text = <<~HEREDOC
Multi-line
string here
HEREDOC
# %w and %i — word/symbol arrays
%w[apple banana cherry] # => ["apple", "banana", "cherry"]
%i[foo bar baz] # => [:foo, :bar, :baz]
30. What is the difference between Array#each and Array#map?
# each — returns original array, used for side effects
[1, 2, 3].each { |n| puts n }
# returns [1, 2, 3]
# map — returns NEW array with transformed values
[1, 2, 3].map { |n| n * 10 }
# => [10, 20, 30]
Use each for side effects (printing, updating external state). Use map when you need a new transformed collection.
31. What is the difference between Hash#merge and Hash#merge!?
a = { x: 1, y: 2 }
b = { y: 10, z: 3 }
a.merge(b) # => { x: 1, y: 10, z: 3 } — returns new hash, a unchanged
a.merge!(b) # => { x: 1, y: 10, z: 3 } — modifies a in place
# With block to resolve conflicts
a.merge(b) { |key, old, new_val| old + new_val }
# => { x: 1, y: 12, z: 3 }
The ! (bang) convention means "modifies receiver in place".
Exception Handling
32. How does exception handling work in Ruby?
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Error: #{e.message}"
rescue ArgumentError, TypeError => e
puts "Type problem: #{e.message}"
rescue => e # catches StandardError and subclasses
puts "Unknown: #{e}"
else
puts "No error, result = #{result}" # only runs if no exception
ensure
puts "Always runs (like finally)" # cleanup, always executes
end
33. What is the exception hierarchy?
Exception
├── SignalException (Interrupt, etc.)
├── ScriptError (SyntaxError, LoadError, NotImplementedError)
├── SystemExit
└── StandardError ← rescue catches this by default
├── RuntimeError
├── ArgumentError
├── TypeError
├── NameError → NoMethodError
├── IOError → EOFError
├── IndexError → KeyError, StopIteration
├── ZeroDivisionError
└── ...
Never rescue Exception — it catches Interrupt (Ctrl+C) and SignalException. Rescue StandardError or specific subclasses.
34. How do you define custom exceptions?
class AppError < StandardError; end
class ValidationError < AppError
attr_reader :field
def initialize(field, message = "is invalid")
@field = field
super("#{field} #{message}")
end
end
begin
raise ValidationError.new(:email, "is not a valid format")
rescue ValidationError => e
puts e.message # => "email is not a valid format"
puts e.field # => :email
end
35. What does retry do in a rescue block?
retry re-runs the begin block from the start:
attempts = 0
begin
attempts += 1
result = flaky_api_call
rescue NetworkError => e
retry if attempts < 3
raise # re-raise if all retries exhausted
end
Concurrency
36. What is the Global Interpreter Lock (GIL) in Ruby?
MRI Ruby (the standard implementation) has a GIL (Global VM Lock): only one thread executes Ruby code at a time, even on multi-core machines.
- I/O-bound work: threads still help — GIL is released during I/O
- CPU-bound work: use
fork(separate processes) or JRuby/TruffleRuby (no GIL)
# Threads — useful for I/O concurrency
threads = 5.times.map do
Thread.new { sleep 1; "done" }
end
threads.map(&:value) # all sleep concurrently — ~1 second total
37. What are fibers?
Fibers are cooperative coroutines — lightweight, manually controlled context switches.
fiber = Fiber.new do
puts "Step 1"
Fiber.yield
puts "Step 2"
Fiber.yield
puts "Step 3"
end
fiber.resume # Step 1
fiber.resume # Step 2
fiber.resume # Step 3
# Fibers as generators
counter = Fiber.new do
n = 0
loop { Fiber.yield(n += 1) }
end
counter.resume # => 1
counter.resume # => 2
counter.resume # => 3
38. What are Ractors (Ruby 3)?
Ractors are actor-like concurrent units that run in parallel without sharing mutable state:
ractor = Ractor.new(5) do |n|
n * 2
end
ractor.take # => 10
# True parallel execution
results = 4.times.map do |i|
Ractor.new(i) { |n| n ** 2 }
end.map(&:take)
# => [0, 1, 4, 9] (computed in parallel)
Ractors enforce isolation — objects must be shareable (frozen or Ractor-safe) to cross boundaries.
Functional Patterns
39. What is method chaining and how does Ruby support it?
Method chaining returns self (or a new object) from each method, enabling pipelines:
" Hello World "
.strip
.downcase
.split
.map(&:capitalize)
.join(" ")
# => "Hello World"
# then/yield_self for value transformation chains
"hello"
.then { |s| s.upcase }
.then { |s| "#{s}!" }
# => "HELLO!"
40. What is tap?
tap yields self to a block and returns self — useful for debugging chains:
[1, 2, 3]
.tap { |a| puts "before: #{a}" }
.map { |n| n * 2 }
.tap { |a| puts "after: #{a}" }
.select(&:odd?)
# before: [1, 2, 3]
# after: [2, 4, 6]
# => []
Practical Patterns
41. What is the difference between require, require_relative, and load?
require "json" # loads from $LOAD_PATH, loads once
require_relative "util" # loads relative to current file, loads once
load "script.rb" # loads every time called, executes top-level code
Use require for gems/stdlib, require_relative for project files, load for scripts you want re-executed.
42. What is Comparable and how do you use it?
Include Comparable and define <=> to get all comparison operators for free:
class Temperature
include Comparable
attr_reader :degrees
def initialize(degrees)
@degrees = degrees
end
def <=>(other)
degrees <=> other.degrees
end
end
temps = [Temperature.new(30), Temperature.new(15), Temperature.new(22)]
temps.min.degrees # => 15
temps.max.degrees # => 30
temps.sort.map(&:degrees) # => [15, 22, 30]
Temperature.new(25).between?(Temperature.new(20), Temperature.new(30)) # => true
43. What is the Struct class?
Struct auto-generates a value object class with accessors, ==, and to_a:
Point = Struct.new(:x, :y) do
def distance_to(other)
Math.sqrt((x - other.x)**2 + (y - other.y)**2)
end
end
p1 = Point.new(0, 0)
p2 = Point.new(3, 4)
p1.distance_to(p2) # => 5.0
p1 == Point.new(0, 0) # => true
44. What is Comparable vs Enumerable?
| Module | Purpose | Required method |
|---|---|---|
Comparable |
Ordering between objects | <=> |
Enumerable |
Collection iteration | each |
Both are mixins. Comparable gives <, >, <=, >=, between?, clamp. Enumerable gives map, select, sort, min, max, reduce, etc.
45. What is Object#freeze vs Ractor.make_shareable?
# freeze — prevents mutation, object stays in current Ractor
str = "hello".freeze
str << " world" # RuntimeError
# Ractor.make_shareable — deep freezes for Ractor sharing
data = { name: "Alice", scores: [1, 2, 3] }
Ractor.make_shareable(data)
# deep-freezes data and all nested objects
Common Gotchas
46. What is the difference between ||= and &&=?
# ||= — assign if nil or false
a = nil
a ||= 10 # a = 10
a ||= 20 # a still 10 (already truthy)
# &&= — assign only if currently truthy
b = 5
b &&= b * 2 # b = 10
c = nil
c &&= 10 # c still nil (&&= skips when falsy)
hash[:key] ||= default is the classic memoization idiom.
47. What is the "frozen string literal" comment?
# frozen_string_literal: true
When placed at the top of a file, all string literals in that file are frozen, improving memory and performance (strings are deduplicated). Modifying them raises FrozenError.
Ruby 3+ makes this opt-in; a future version may make it the default.
48. What is * (splat) and ** (double splat)?
# Single splat — collects extra positional args
def greet(first, *rest)
puts "#{first} and #{rest.join(", ")}"
end
greet("Alice", "Bob", "Carol") # Alice and Bob, Carol
# Array unpacking
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2,3,4], last=5
# Double splat — keyword arguments / hash unpacking
def configure(host:, port: 80, **options)
puts options.inspect
end
configure(host: "localhost", debug: true)
# => { debug: true }
# Hash unpacking
defaults = { timeout: 30, retries: 3 }
configure(host: "example.com", **defaults)
49. What are common Ruby anti-patterns?
| Anti-pattern | Problem | Fix |
|---|---|---|
Rescuing Exception |
Catches Interrupt, hides signals |
Rescue StandardError or specific |
Using method_missing for everything |
Slow, hides NoMethodError | Use define_method or real methods |
| Monkey patching without refinements | Breaks gems, hard to debug | Use refine for scoped patches |
eval(string) |
Security risk, slow | Use send, define_method, blocks |
| Mutable default arguments | Shared state across calls | Use nil default, assign inside |
Not defining respond_to_missing? |
respond_to? lies after method_missing |
Always pair them |
Over-using globals ($var) |
Hard to test and reason about | Use dependency injection |
| Ignoring frozen string literals | Memory waste, unintended mutation | Add # frozen_string_literal: true |
50. Ruby vs Python vs JavaScript — quick comparison
| Dimension | Ruby | Python | JavaScript |
|---|---|---|---|
| Typing | Dynamic, duck-typed | Dynamic, duck-typed | Dynamic |
| Falsy values | nil, false only |
None, 0, "", [], etc. |
Many (0, "", null, undefined, NaN) |
| Blocks/closures | First-class blocks + procs/lambdas | Lambdas (1-expr only), functions | Functions, arrow functions |
| Mixins | include/extend/prepend |
Multiple inheritance | Prototypes, mixins via Object.assign |
| Metaprogramming | Very strong (open classes, method_missing) |
Moderate (__getattr__, decorators) |
Proxies, Reflect |
| Main use case | Web (Rails), scripting | Data science, ML, scripting | Web frontend + backend (Node) |
| Concurrency | GIL (MRI), Ractors (Ruby 3) | GIL (CPython), asyncio | Event loop, Workers |
| Package manager | Bundler + RubyGems | pip + venv/poetry | npm/yarn/pnpm |
FAQ
Q: Is Ruby strictly object-oriented?
A: Yes — every value in Ruby is an object, including integers, nil, true, and false. There are no primitive types. Even classes are objects (instances of Class).
Q: What is the difference between nil in Ruby and null in other languages?
A: nil is an object (NilClass.new would fail because there's only one nil). It has methods like nil.to_i # => 0, nil.to_s # => "", nil.nil? # => true. In Java/C, null is just the absence of a pointer.
Q: When should I use a Struct vs a plain class?
A: Struct is ideal for value objects — small data containers with few/no methods where equality is based on field values. Use a full class when you need complex initialization, inheritance, or many methods.
Q: What is duck typing?
A: Ruby doesn't check an object's class — it checks whether it responds to the methods you call. "If it quacks like a duck, it's a duck." Use respond_to?(:method_name) instead of is_a?(SomeClass) for flexible, decoupled code.
Q: What is the difference between throw/catch and raise/rescue in Ruby?
A: raise/rescue is for exceptions and errors. throw/catch is for non-local jumps (e.g., breaking out of nested loops or early exit from a computation) without the overhead of exception handling.
result = catch(:done) do
[1,2,3,4,5].each do |n|
[10,20,30].each do |m|
throw :done, n * m if n * m > 50
end
end
end
# => 60 (3 * 20)
Q: What is Bundler and why do you use it?
A: Bundler manages gem dependencies for a project. Gemfile declares what gems and versions you need; Gemfile.lock pins exact versions for reproducible installs. Run bundle exec to ensure the correct gem versions are used. Essential for any non-trivial Ruby project.