Ruby Interview Questions and Answers
Master Ruby interviews with questions on object-oriented programming, metaprogramming, closures, modules, mixins, and Ruby 3+ features.
📋 Jump to Question
Ruby Basics & Philosophy
Q1: What is Ruby and what are its main characteristics?
A: Ruby is a dynamic, object-oriented, interpreted programming language created by Yukihiro "Matz" Matsumoto. Key characteristics:
- Everything is an object
- Dynamic typing and duck typing
- Garbage collection
- Blocks, Procs, and Lambdas for functional programming
- Open classes (can modify classes at runtime)
- Elegant syntax designed for developer happiness
Q2: What does "Everything is an object in Ruby" mean?
A: In Ruby, absolutely everything is an object - including primitive types, classes, modules, and even nil. Every object has methods and can send messages.
# Even numbers are objects
5.times { puts "Hello" } # 5 is an instance of Integer
# nil is an object
nil.nil? # true
nil.class # NilClass
# Classes themselves are objects
String.class # Class
Class.class # Class
Q3: What is the difference between puts, print, and p?
A:
puts- prints with a newline, callsto_son objectsprint- prints without a newline, callsto_son objectsp- prints with a newline, callsinspect(more detailed for debugging)
puts "Hello" # Hello\n
print "Hello" # Hello (no newline)
p "Hello" # "Hello"\n (shows quotes for strings)
array = [1,2,3]
puts array # 1\n2\n3\n
p array # [1, 2, 3]\n
Q4: Explain Ruby's naming conventions
A:
- Local variables:
my_variable,counter - Instance variables:
@name,@age(start with @) - Class variables:
@@count,@@total(start with @@) - Constants:
MAX_SIZE,API_KEY(start with capital) - Class names:
MyClass,UserController(CamelCase) - Methods that return boolean:
valid?,empty?(end with ?) - Dangerous/modifying methods:
save!,update!(end with !) - Setter methods:
name=,age=(end with =)
Variables and Data Types
Q5: What are the variable scopes in Ruby?
A:
$global = "I'm global" # Global variable - starts with $
@@class_var = "Class var" # Class variable - starts with @@
@instance = "Instance var" # Instance variable - starts with @
local = "Local var" # Local variable - lowercase or _
CONSTANT = "Constant" # Constant - starts with uppercase
Q6: Explain the difference between String and Symbol
A:
- String: Mutable, each string creates a new object in memory
- Symbol: Immutable, same symbol always refers to the same object
"hello".object_id == "hello".object_id # false (different objects)
:hello.object_id == :hello.object_id # true (same object)
# Memory comparison
5.times { puts "test".object_id } # All different
5.times { puts :test.object_id } # All the same
Use symbols for: Hash keys, identifiers, method names, labels Use strings for: Data that changes, user input, text manipulation
Q7: What is the difference between ==, ===, equal?, and eql??
A:
==- value equality (most common)===- case equality (used in case statements)equal?- identity equality (same object_id)eql?- hash equality (used for Hash keys)
# == (value equality)
"hello" == "hello" # true
# === (case equality)
(1..5) === 3 # true
String === "hello" # true
# equal? (identity)
a = "hello"
b = "hello"
a.equal?(b) # false (different objects)
# eql? (hash equality)
"hello".eql?("hello") # true
Q8: What are the different types of variables and their scope?
A:
class Example
@@class_count = 0 # Class variable - shared across all instances
def initialize
@instance_var = "unique" # Instance variable - unique to each object
local_var = "temporary" # Local variable - only within method
end
def show
puts @instance_var # Works
puts local_var # Error! Not accessible
end
end
$global_var = "everywhere" # Global - accessible anywhere
Control Structures
Q9: What's the difference between if and unless?
A: unless is the opposite of if - executes code when condition is false
# These are equivalent
if !user.active
puts "User inactive"
end
unless user.active
puts "User inactive"
end
# Can be used as modifiers
puts "Done" if completed?
puts "Not done" unless completed?
Q10: Explain Ruby's ternary operator
A: Short form of if-else
age >= 18 ? "Adult" : "Minor"
# Same as:
if age >= 18
"Adult"
else
"Minor"
end
Q11: What's the difference between while and until?
A: while runs while condition is true, until runs until condition becomes true
# While - runs while true
i = 0
while i < 5
puts i
i += 1
end
# Until - runs until true (while false)
i = 0
until i == 5
puts i
i += 1
end
Arrays and Hashes
Q12: How do you iterate over an array?
A: Multiple ways:
array = [1, 2, 3, 4, 5]
# Most common
array.each { |num| puts num }
# With index
array.each_with_index { |num, idx| puts "#{idx}: #{num}" }
# Map/Collect - transform
doubled = array.map { |num| num * 2 }
# Select - filter
evens = array.select { |num| num.even? }
# Reject - opposite of select
odds = array.reject { |num| num.even? }
Q13: How do you access and modify hash elements?
A:
person = { name: "John", age: 30 }
# Access
person[:name] # "John"
person.fetch(:name) # "John" (raises error if key missing)
person[:city] # nil
# Default values
person.fetch(:city, "Unknown") # "Unknown"
Hash.new("default") # Hash with default value
# Modify
person[:age] = 31
person[:city] = "New York"
# Iterate
person.each { |key, value| puts "#{key}: #{value}" }
Q14: What's the difference between Array#each and Array#map?
A:
each- iterates, returns original array (for side effects)map- iterates, returns new transformed array
numbers = [1, 2, 3]
# each - returns original array
result = numbers.each { |n| n * 2 }
result == numbers # true
# map - returns new array
result = numbers.map { |n| n * 2 }
result == numbers # false, result = [2, 4, 6]
Q15: What are array and hash default values?
A:
# Array default
arr = Array.new(3, "default")
arr[0] = "changed"
puts arr # ["changed", "default", "default"] (independent)
# Watch out - same object reference
arr = Array.new(3, [])
arr[0] << "item"
puts arr # [["item"], ["item"], ["item"]] (all reference same array)
# Hash default
hash = Hash.new(0)
hash[:count] += 1 # Works even if :count didn't exist
puts hash[:count] # 1
Methods
Q16: What are the different ways to define and call methods?
A:
# Basic method
def greet(name)
"Hello, #{name}"
end
# With default parameters
def greet(name = "World")
"Hello, #{name}"
end
# With keyword arguments
def greet(name:, age: nil)
"Hello, #{name}"
end
# With variable arguments
def greet(*names)
names.each { |name| puts "Hello, #{name}" }
end
# With block
def greet(&block)
block.call if block_given?
end
# Calling
greet("John")
greet(name: "John")
greet("John", "Jane", "Joe")
greet { puts "Hello" }
Q17: What is the difference between implicit and explicit return?
A: Ruby always returns the last evaluated expression (implicit return)
# Implicit return - returns last line
def add(a, b)
a + b # Automatically returns this
end
# Explicit return - returns immediately
def check_age(age)
return "Invalid" if age < 0
age >= 18 ? "Adult" : "Minor"
end
# Multiple returns
def get_coordinates
[10, 20] # Returns array
# or
return 10, 20 # Returns multiple values as array
end
x, y = get_coordinates # Destructuring
Q18: What are predicate methods?
A: Methods that return boolean, conventionally ending with ?
# Ruby built-in
array.empty? # Is array empty?
string.include?("a") # Does string include "a"?
object.nil? # Is object nil?
# Custom predicate
class Person
def adult?
age >= 18
end
def valid?
!name.nil? && age > 0
end
end
Classes and Objects
Q19: How do you define a class with getters and setters?
A:
class Person
# Shortcut for getter/setter
attr_accessor :name, :age
# Only getter
attr_reader :id
# Only setter
attr_writer :password
# Constructor
def initialize(name, age)
@name = name
@age = age
@id = generate_id
end
# Instance method
def introduce
"I'm #{@name}, #{@age} years old"
end
# Class method
def self.species
"Human"
end
private
def generate_id
rand(1000..9999)
end
end
# Usage
person = Person.new("John", 30)
person.name # "John" (getter)
person.name = "Jane" # "Jane" (setter)
person.id # Can get, but can't set
person.introduce # Instance method
Person.species # Class method
Q20: What's the difference between class and instance variables?
A:
class Counter
@@total_count = 0 # Class variable - shared
def initialize
@count = 0 # Instance variable - unique
end
def increment
@count += 1
@@total_count += 1
end
def show
puts "Instance: #{@count}, Total: #{@@total_count}"
end
def self.total
@@total_count
end
end
c1 = Counter.new
c2 = Counter.new
c1.increment # Instance: 1, Total: 1
c2.increment # Instance: 1, Total: 2
puts Counter.total # 2 (class method can access @@total_count)
Q21: What is self in Ruby?
A: self refers to the current object context
class Example
puts "In class: #{self}" # Example
def instance_method
puts "In instance: #{self}" # <Example:0x...>
end
def self.class_method
puts "In class method: #{self}" # Example
end
# Setter method using self
def name=(value)
@name = value
# Without self, Ruby thinks it's a local variable
# self.name = value # This would call the setter recursively
end
end
Modules and Mixins
Q22: What's the difference between a class and a module?
A:
- Class: Can be instantiated, can inherit
- Module: Can't be instantiated, can't inherit, used for namespacing and mixins
# Module for namespacing
module Admin
class User
# Admin::User
end
end
# Module for mixin
module Loggable
def log(message)
puts "[LOG] #{message}"
end
end
class Service
include Loggable # Adds as instance methods
end
Service.new.log("Started") # Works
class AnotherService
extend Loggable # Adds as class methods
end
AnotherService.log("Started") # Works
Q23: Explain include vs prepend vs extend
A:
module M
def hello
"from M"
end
end
class Base
def hello
"from Base"
end
end
# include - adds module after class in ancestor chain
class A < Base
include M
end
A.ancestors # [A, M, Base, ...]
# prepend - adds module before class in ancestor chain
class B < Base
prepend M
end
B.ancestors # [M, B, Base, ...]
# extend - adds module methods as class methods
class C
extend M
end
C.hello # "from M" (class method)
Common Ruby Idioms
Q24: What does ||= (or-equals) do?
A: Conditional assignment - assigns only if variable is false or nil
# These are equivalent:
@count ||= 0
@count = @count || 0
# Common patterns
def memoized_value
@cached ||= expensive_operation
end
# For boolean values, use defined? instead
@enabled = false
@enabled ||= true # This would set to true (WRONG!)
@enabled = true if @enabled.nil? # Better
Q25: What's the safe navigation operator (&.)?
A: Ruby 2.3+ - calls method only if object is not nil
user = nil
user&.name # nil (instead of NoMethodError)
# Without &.
if user && user.address && user.address.city
city = user.address.city
end
# With &.
city = user&.address&.city
Q26: Explain the splat operator (*)
A: Converts array to arguments list and vice versa
# Collect arguments
def greet(*names)
names.join(", ")
end
greet("John", "Jane", "Joe") # names = ["John", "Jane", "Joe"]
# Expand array
args = ["John", 30]
def person(name, age)
puts "#{name} is #{age}"
end
person(*args) # Expands array to arguments
# With hash (double splat)
def configure(**options)
options.each { |k,v| puts "#{k}=#{v}" }
end
config = {host: "localhost", port: 3000}
configure(**config)
Q27: What are code blocks and how do you use them?
A: Blocks are chunks of code that can be passed to methods
# Two syntaxes
3.times { |i| puts i } # { } for one-liners
3.times do |i| # do...end for multi-line
puts i
puts "Hello"
end
# Yield to block
def repeat(n)
n.times { yield if block_given? }
end
repeat(3) { puts "Hello" }
# Capture block as proc
def processor(&block)
block.call if block
end
Quick Practice Questions
Q28: What will this output?
def test
x = 10
if false
x = 20
end
x
end
puts test
A: 10 - The if false block never executes, so x remains 10
Q29: What's the result?
arr = [1,2,3]
puts arr.map { |x| x.even? }
puts arr.select { |x| x.even? }
A:
mapreturns[false, true, false]selectreturns[2]
Q30: What's the difference?
def method1
{key: "value"}
end
def method2
{key: "value"} if true
end
A:
method1always returns a hashmethod2returns a hash when condition is true, but returnsnilif condition is false
Q31: What will this return?
class Test
@value = 10
def self.value
@value
end
end
puts Test.value
A: 10 - This is a class instance variable, not a class variable
Q32: Fix this code:
hash = {a: 1, b: 2}
hash.each { |k,v| k.upcase! } # Why does this fail?
A: Symbols are immutable, so upcase! fails. Fix by using strings as keys:
hash = {"a" => 1, "b" => 2}
hash.each { |k,v| k.upcase! }
# Or create a new hash with transformed keys
new_hash = hash.transform_keys { |k| k.to_s.upcase.to_sym }
Blocks, Procs, and Lambdas
Q1: What is the difference between a Block, Proc, and Lambda?
A:
| Feature | Block | Proc | Lambda |
|---------|-------|------|--------|
| Object? | No | Yes | Yes |
| Arity | Loose | Loose | Strict |
| Return | Returns from method | Returns from method | Returns from lambda |
| Creation | { \|x\| x*2 } | Proc.new { \|x\| x*2 } | ->(x) { x*2 } |
# Block - not an object, can't be stored in variable
[1,2,3].each { |n| puts n }
# Proc - object, can be stored
my_proc = Proc.new { |n| puts n }
my_proc.call(1)
# Lambda - object, strict arity
my_lambda = ->(n) { puts n }
my_lambda.call(1)
Q2: Demonstrate the return behavior difference between Proc and Lambda
A:
def proc_return
proc = Proc.new { return "Proc returned" }
proc.call
"This will never be executed"
end
def lambda_return
lambda = -> { return "Lambda returned" }
lambda.call
"This WILL be executed"
end
puts proc_return # "Proc returned"
puts lambda_return # "This WILL be executed"
# Proc returns from enclosing method
# Lambda returns only from itself
Q3: What is arity and how does it differ between Proc and Lambda?
A:
# Lambda - strict arity
lambda = ->(x, y) { x + y }
lambda.call(1, 2) # 3
lambda.call(1) # ArgumentError (wrong number)
lambda.call(1,2,3) # ArgumentError
# Proc - loose arity
proc = Proc.new { |x, y| x + y }
proc.call(1, 2) # 3
proc.call(1) # 1 + nil = TypeError (nil can't be coerced)
proc.call(1,2,3) # 3 (ignores extra argument)
# Safe proc with default
safe_proc = Proc.new { |x=0, y=0| x + y }
safe_proc.call(1) # 1
Q4: Explain closure in Ruby with examples
A: A closure is a function that remembers the environment in which it was created.
def make_counter
count = 0
Proc.new { count += 1 }
end
counter1 = make_counter
puts counter1.call # 1
puts counter1.call # 2
puts counter1.call # 3
counter2 = make_counter
puts counter2.call # 1 (separate closure)
# Closure captures local variables
def multiplier(factor)
->(n) { n * factor } # factor is captured
end
double = multiplier(2)
triple = multiplier(3)
puts double.call(5) # 10
puts triple.call(5) # 15
Q5: How do you convert between blocks, procs, and lambdas?
A:
# Block to Proc
def method(&block)
block.class # Proc
end
# Proc to block
my_proc = Proc.new { |x| puts x }
[1,2,3].each(&my_proc) # & converts proc to block
# Lambda to Proc
my_lambda = ->(x) { x * 2 }
my_proc = my_lambda # Lambda is a subclass of Proc
# Check if it's a lambda
my_lambda.lambda? # true
my_proc.lambda? # false
# Symbol to Proc
[1,2,3].map(&:to_s) # ["1", "2", "3"]
# Equivalent to: [1,2,3].map { |n| n.to_s }
Q6: What are the performance implications of using blocks vs procs?
A:
require 'benchmark'
array = (1..1000000).to_a
Benchmark.bm do |x|
# Block - fastest for single use
x.report("Block:") { array.map { |n| n * 2 } }
# Proc - slight overhead
double_proc = Proc.new { |n| n * 2 }
x.report("Proc: ") { array.map(&double_proc) }
# Lambda - similar to proc
double_lambda = ->(n) { n * 2 }
x.report("Lambda:") { array.map(&double_lambda) }
end
# Symbol to proc - convenient but slower for simple operations
x.report("Symbol:") { array.map(&:to_s) }
Q7: Explain yield vs block.call
A:
# yield - cleaner syntax, slight performance benefit
def with_yield
puts "Before"
yield if block_given?
puts "After"
end
# block.call - more explicit, can pass block around
def with_call(&block)
puts "Before"
block.call if block
puts "After"
end
# yield is faster for simple cases
require 'benchmark'
Benchmark.bm do |x|
x.report("yield:") { 1000000.times { with_yield { 1 + 1 } } }
x.report("call: ") { 1000000.times { with_call { 1 + 1 } } }
end
Metaprogramming
Q8: What is method_missing and how do you use it properly?
A: method_missing is called when a method doesn't exist. Always override respond_to_missing? for consistency.
class DynamicMethod
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?("find_by_")
attribute = method_name.to_s.sub("find_by_", "")
puts "Searching by #{attribute}: #{args.first}"
else
super # Important: call super for unhandled cases
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("find_by_") || super
end
end
obj = DynamicMethod.new
obj.find_by_name("John") # Works
obj.respond_to?(:find_by_name) # true
obj.some_other_method # NoMethodError (calls super)
Q9: Explain define_method and when to use it
A: define_method creates methods dynamically at runtime.
class User
ATTRIBUTES = [:name, :email, :age]
ATTRIBUTES.each do |attr|
# Define getter
define_method(attr) do
instance_variable_get("@#{attr}")
end
# Define setter
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
user = User.new
user.name = "John"
puts user.name # "John"
# More complex example - creating CRUD methods dynamically
class Repository
[:find, :save, :update, :delete].each do |action|
define_method("#{action}_user") do |*args|
puts "Performing #{action} on user with args: #{args}"
# Actual implementation
end
end
end
repo = Repository.new
repo.find_user(1)
repo.save_user({name: "John"})
Q10: What is method_missing vs define_method performance?
A:
require 'benchmark'
class MethodMissing
def method_missing(name, *args)
if name == :dynamic_method
"result"
else
super
end
end
end
class DefineMethod
define_method(:dynamic_method) { "result" }
end
mm = MethodMissing.new
dm = DefineMethod.new
Benchmark.bm do |x|
x.report("method_missing:") { 1000000.times { mm.dynamic_method } }
x.report("define_method: ") { 1000000.times { dm.dynamic_method } }
end
# define_method is significantly faster because:
# - method_missing requires method lookup failure first
# - method_missing adds overhead of checking each call
Q11: Explain class_eval vs instance_eval
A:
class Person
end
# instance_eval - evaluates in context of an instance
person = Person.new
person.instance_eval do
def name
"John"
end
end
puts person.name # "John" (singleton method)
# class_eval - evaluates in context of the class
Person.class_eval do
def species
"Human"
end
end
puts Person.new.species # "Human" (instance method)
# module_eval (alias for class_eval)
Person.module_eval do
def self.total_count
100
end
end
puts Person.total_count # "100" (class method)
# Key differences:
# instance_eval: changes self to the instance, creates singleton methods
# class_eval: changes self to the class, creates instance methods
Q12: What are hooks in Ruby? Give examples
A: Hooks are methods called automatically when certain events occur.
module TrackChanges
def self.included(base)
puts "#{base} included me"
base.extend(ClassMethods)
end
def self.extended(base)
puts "#{base} extended me"
end
module ClassMethods
def inherited(subclass)
puts "#{subclass} inherited from #{self}"
end
def method_added(name)
puts "Method #{name} added to #{self}"
end
end
end
class Parent
extend TrackChanges::ClassMethods
def self.inherited(subclass)
super
puts "Custom inherited logic for #{subclass}"
end
def self.method_missing(name, *args)
puts "Method missing in class: #{name}"
end
def self.respond_to_missing?(name, include_all)
true
end
end
class Child < Parent
def test_method
end
end
# Other important hooks:
class Base
# Called when module is included
def self.included(base)
end
# Called when module is prepended
def self.prepended(base)
end
# Called when object is extended with module
def self.extended(obj)
end
# Called when method is undefined
def self.method_removed(name)
end
# Called when method is undefined with undef
def self.method_undefined(name)
end
end
Q13: What is const_missing and how would you use it?
A: const_missing is called when referencing a constant that doesn't exist.
class AutoLoadModule
def self.const_missing(name)
puts "Loading #{name} dynamically"
# Auto-load from file
file_name = name.to_s.downcase
if File.exist?("#{file_name}.rb")
require_relative file_name
const_get(name) # Return the now-defined constant
else
# Create dynamic constant
const_set(name, Class.new do
define_method(:initialize) do |*args|
puts "Dynamically created #{name} with #{args}"
end
end)
end
end
end
# These will trigger const_missing
AutoLoadModule::User # Loads user.rb or creates dynamically
AutoLoadModule::Product.new # Creates dynamic class
# Rails-style autoloading
module ActiveSupport
module Autoload
def autoload(const_name, path)
(@autoloads ||= {})[const_name] = path
end
def const_missing(name)
if path = @autoloads[name]
require path
const_get(name)
else
super
end
end
end
end
Q14: Explain the concept of "Open Classes" and monkey patching
A: Ruby classes are open - you can modify them anytime.
# Open class - adding methods to existing classes
class String
def shout
upcase + "!"
end
def word_count
split.size
end
end
"hello world".shout # "HELLO WORLD!"
"one two three".word_count # 3
# Monkey patching - overriding existing methods
class Array
alias old_plus +
def +(other)
puts "Adding #{other} to #{self}"
old_plus(other)
end
end
[1,2] + [3,4] # Logs message, then [1,2,3,4]
# Refinements - safer alternative (Ruby 2.0+)
module StringExtensions
refine String do
def shout
upcase + "!"
end
end
end
using StringExtensions
puts "hello".shout # "HELLO!" (only in this scope)
# Without using
puts "hello".shout # NoMethodError
Q15: What are send and public_send and when to use them?
A: send calls methods dynamically, can call private methods.
class Calculator
def add(a, b)
a + b
end
private
def secret_formula
42
end
end
calc = Calculator.new
# Dynamic method calls
operation = :add
calc.send(operation, 5, 3) # 8
# send can call private methods
calc.send(:secret_formula) # 42
# public_send respects privacy
calc.public_send(:secret_formula) # NoMethodError
# Real-world use case: DSLs
class DynamicConfig
attr_accessor :name, :age, :email
def configure(&block)
instance_eval(&block)
end
def method_missing(name, *args)
if name.to_s.end_with?("=")
# Dynamic attribute setting
instance_variable_set("@#{name.to_s.chop}", args.first)
elsif args.empty?
# Dynamic attribute reading
instance_variable_get("@#{name}")
else
super
end
end
end
config = DynamicConfig.new
config.configure do
name "John"
age 30
email "john@example.com"
end
puts config.name # "John"
Garbage Collection
Q16: How does Ruby's Garbage Collector work?
A: Ruby uses a mark-and-sweep garbage collector with generational collection.
# Basic GC concepts
class GarbageCollection
def demonstrate
# Objects created in this method
data = []
10000.times { data << "string" }
# When method ends, 'data' is eligible for GC
# (unless referenced elsewhere)
end
def memory_leak
@cache ||= []
@cache << "leak" # This persists in @cache
end
end
# GC can be controlled
GC.start # Manual GC run
GC.disable # Turn off GC
GC.enable # Turn on GC
GC.stress = true # Stress test mode
# GC statistics
GC.stat # Hash of GC metrics
GC.latest_gc_info # Info about last GC
# Ruby 2.x+ uses generational GC
# - Young generation: new objects (collected frequently)
# - Old generation: surviving objects (collected less often)
Q17: Explain mark-and-sweep garbage collection
A:
# Conceptual mark-and-sweep
class SimpleGC
attr_accessor :references
attr_reader :marked
def initialize
@references = []
@marked = false
end
def refer_to(obj)
@references << obj
end
end
# Mark phase - traverse object graph
def mark(root)
return if root.marked
root.marked = true
root.references.each { |ref| mark(ref) }
end
# Sweep phase - collect unmarked objects
def sweep(objects)
objects.reject! do |obj|
if obj.marked
obj.marked = false # Reset for next cycle
false # Keep
else
true # Remove (collect)
end
end
end
# Ruby's actual GC is more sophisticated:
# - Tri-color marking (white, grey, black)
# - Incremental marking to avoid pauses
# - Write barriers to track modifications
Q18: How do you identify and fix memory leaks in Ruby?
A:
# Tools and techniques
require 'objspace'
class MemoryLeakDetector
def self.analyze
# Track object allocation
ObjectSpace.count_objects
# Track specific objects
strings_before = ObjectSpace.each_object(String).count
# Operation that might leak
10000.times { |i| @leak ||= []; @leak << "leak#{i}" }
strings_after = ObjectSpace.each_object(String).count
puts "Strings created: #{strings_after - strings_before}"
end
def self.find_leaks
# Find objects that shouldn't exist
ObjectSpace.each_object(Array) do |arr|
if arr.size > 1000
puts "Large array: #{arr.object_id} size: #{arr.size}"
puts caller_locations if arr.respond_to?(:caller_locations)
end
end
end
end
# Common leak patterns and fixes:
# 1. Class variables accumulation
class LeakyClass
@@cache = {}
def self.process(data)
@@cache[data.object_id] = data # Leaks unless cleaned
end
end
# Fix: Use WeakRef or limit cache size
require 'weakref'
class FixedClass
@cache = {}
def self.process(data)
@cache[data.object_id] = WeakRef.new(data)
@cache.delete_if { |_, v| !v.weakref_alive? }
end
end
# 2. Callback accumulation
class EventEmitter
def initialize
@callbacks = []
end
def on_event(&block)
@callbacks << block # Blocks keep context alive
end
end
# Fix: Use weak references for callbacks
require 'weakref'
class FixedEmitter
def initialize
@callbacks = []
end
def on_event(&block)
@callbacks << WeakRef.new(block)
end
def emit
@callbacks.reject! { |ref| !ref.weakref_alive? }
@callbacks.each { |ref| ref.weakref_alive? && ref.call }
end
end
Q19: What's the difference between Ruby 2.x and 3.x garbage collection?
A:
# Ruby 2.x - Generational GC
# - Young generation (eden + tomb)
# - Old generation
# - Major and minor GC cycles
# Ruby 3.x - GC improvements
# - Incremental marking (shorter pauses)
# - Variable heap allocation
# - Compaction support
# Ruby 3.0+ features
class GC3Features
def demonstrate
# Compaction - defragment heap
GC.compact
# Measure time
GC.measure_total_time = true
GC.total_time # Returns total GC time
# Verify heap
GC.verify_compaction_references
# Statistics
GC.stat(:total_allocated_objects)
GC.stat(:malloc_increase_bytes)
end
end
# Ruby 3.2+ - M:N threads affect GC
# - Reduced memory per thread
# - Better concurrency handling
Q20: How does object allocation affect GC performance?
A:
require 'benchmark'
class AllocationOptimization
# Bad: Creates many intermediate objects
def slow_method(data)
result = []
data.each do |item|
result << item.to_s.upcase.strip
end
result
end
# Better: Reuses objects when possible
def fast_method(data)
data.map! do |item|
item = item.to_s
item.upcase!
item.strip!
item
end
end
# Benchmark allocation differences
def self.benchmark_allocation
data = (1..10000).to_a
GC.disable # Disable GC for measurement
before = GC.stat(:total_allocated_objects)
slow_method(data)
after = GC.stat(:total_allocated_objects)
puts "Slow method allocated: #{after - before}"
before = GC.stat(:total_allocated_objects)
fast_method(data.dup)
after = GC.stat(:total_allocated_objects)
puts "Fast method allocated: #{after - before}"
GC.enable
end
# Tips for reducing allocations:
# 1. Use mutable strings with !
# 2. Reuse objects
# 3. Use symbols instead of strings for identifiers
# 4. Use freeze for immutable constants
# 5. Use lazy enumerators for large datasets
end
CONSTANT = "frozen string".freeze # Won't be reallocated
Advanced Object-Oriented Programming
Q21: Explain the difference between ==, equal?, eql?, and ===
A:
class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
# Value equality
def ==(other)
other.is_a?(Person) && name == other.name && age == other.age
end
# Hash equality - needed for Hash keys
def eql?(other)
self == other
end
# Hash generation - must be consistent with eql?
def hash
[name, age].hash
end
# Case equality - used in case statements
def ===(other)
return false unless other.is_a?(Person)
age_range = case name
when "Child" then 0..12
when "Teen" then 13..19
when "Adult" then 20..
else 0..
end
age_range === other.age
end
end
# Demonstration
p1 = Person.new("John", 30)
p2 = Person.new("John", 30)
p3 = Person.new("John", 15)
puts p1 == p2 # true (value equality)
puts p1.equal?(p2) # false (different objects)
puts p1.eql?(p2) # true (hash equality)
# Case statement using ===
case Person.new("Child", 10)
when p1 # Uses ===
puts "Matches John"
when p3
puts "Matches teen"
else
puts "No match"
end
Q22: What are initialize, initialize_copy, initialize_clone, and initialize_dup?
A:
class CustomObject
attr_accessor :data, :metadata
def initialize(data, metadata = {})
@data = data
@metadata = metadata
@created_at = Time.now
@id = SecureRandom.uuid
end
# Called when object is duplicated (dup)
def initialize_dup(original)
super
@data = @data.dup # Deep copy mutable data
@metadata = @metadata.dup
@id = SecureRandom.uuid # New ID for duplicate
puts "Duplicating object"
end
# Called when object is cloned (clone)
def initialize_clone(original)
super
@data = @data.dup
@metadata = @metadata.dup
# clone preserves frozen state and singleton methods
puts "Cloning object"
end
# Called when object is copied
def initialize_copy(original)
super
puts "Copying object"
end
def display
puts "ID: #{@id}, Data: #{@data}, Metadata: #{@metadata}"
end
end
original = CustomObject.new([1,2,3], {key: "value"})
original.display
dup_obj = original.dup
dup_obj.display
clone_obj = original.clone
clone_obj.display
# Differences:
# - dup doesn't copy singleton methods, clone does
# - dup doesn't preserve frozen state, clone does
# - Both call initialize_copy (which calls initialize_dup/clone)
Q23: Explain the concept of "Duck Typing" with examples
A: "If it walks like a duck and quacks like a duck, it's a duck."
# Duck typing - concerned with what object can DO, not what it IS
class Duck
def quack
"Quack!"
end
def walk
"Waddle waddle"
end
end
class Person
def quack
"I'm pretending to quack!"
end
def walk
"Walking normally"
end
end
class Robot
def quack
"BEEP QUACK BEEP"
end
def walk
"MOTORS ACTIVATED"
end
end
# This method doesn't care about the class
def make_it_quack_and_walk(thing)
puts thing.quack
puts thing.walk
end
make_it_quack_and_walk(Duck.new) # Works
make_it_quack_and_walk(Person.new) # Works
make_it_quack_and_walk(Robot.new) # Works
# Real-world example
class PaymentProcessor
def process(payment_method, amount)
# Doesn't care if payment_method is CreditCard, PayPal, or Bitcoin
# As long as it responds to charge, refund, etc.
if payment_method.respond_to?(:charge)
payment_method.charge(amount)
else
raise "Can't process this payment"
end
end
end
class CreditCard
def charge(amount)
"Charging $#{amount} to credit card"
end
end
class PayPal
def charge(amount)
"Processing $#{amount} via PayPal"
end
end
Q24: What is the difference between private, protected, and public methods?
A:
class Parent
public
def public_method
"Public: Anyone can call me"
end
protected
def protected_method
"Protected: Only instances of this class and subclasses"
end
private
def private_method
"Private: Can't call with explicit receiver"
end
# Private setter example
private
def name=(value)
@name = value
end
def test_access(other)
puts private_method # Works (implicit self)
puts self.private_method # Error (explicit receiver)
puts protected_method # Works
puts self.protected_method # Works
# Can call protected on other instance of same class
puts other.protected_method # Works
puts other.private_method # Error
end
end
class Child < Parent
def test_parent_access
puts protected_method # Works (inherited)
# puts private_method # Error (private not inherited?)
# Actually, private methods ARE inherited, but can't be called with receiver
# This works:
puts private_method # implicit self
end
def try_protected(other_parent, other_child)
puts other_parent.protected_method # Works (same class hierarchy)
puts other_child.protected_method # Works
end
end
# Summary:
# public: No restrictions
# protected: Can be called by any instance of class or subclasses
# private: Can only be called without explicit receiver
Modules and Mixins Deep Dive
Q25: What is the method lookup path and how does super work?
A:
module A
def method
"A"
end
end
module B
def method
"B" + super
end
end
class Parent
def method
"Parent"
end
end
class Child < Parent
include A
prepend B
def method
"Child" + super
end
end
# Check ancestors
puts Child.ancestors.inspect
# [B, Child, A, Parent, Object, Kernel, BasicObject]
# Method lookup:
# 1. B#method (prepended)
# 2. Child#method
# 3. A#method (included)
# 4. Parent#method
child = Child.new
puts child.method # "BChildAParent"
# super behavior:
# - Without args: passes original args
# - With empty parens: passes no args
# - With args: passes specified args
class Example
def initialize(name, age)
@name = name
@age = age
end
end
class SubExample < Example
def initialize(name, age, city)
super(name, age) # Passes only name and age to parent
@city = city
end
end
Q26: Explain include, prepend, and extend with module callbacks
A:
module Trackable
def self.included(base)
puts "#{base} included me"
base.extend(ClassMethods)
base.include(InstanceMethods)
end
def self.prepended(base)
puts "#{base} prepended me"
end
def self.extended(base)
puts "#{base} extended me"
end
module ClassMethods
def class_method
"Class method added by include"
end
def self.extended(base)
puts "ClassMethods extended into #{base}"
end
end
module InstanceMethods
def instance_method
"Instance method added by include"
end
end
# Regular module methods (can't be called directly)
def mixed_in_method
"This method is mixed in"
end
end
class Test
include Trackable
# Callback order:
# 1. Trackable.included(Test)
# 2. Test.extend(ClassMethods)
# 3. Test.include(InstanceMethods)
end
class PrependTest
prepend Trackable
# Trackable.prepended(PrependTest) called
end
class ExtendTest
extend Trackable
# Trackable.extended(ExtendTest) called
end
# Usage
puts Test.new.mixed_in_method # Works (included)
puts Test.class_method # Works (class method from include)
puts Test.new.instance_method # Works (from InstanceMethods)
puts PrependTest.new.mixed_in_method # Works (prepended)
puts ExtendTest.mixed_in_method # Works (extended as class method)
Q27: What are Refinements and when should you use them?
A: Refinements provide a way to scope monkey patches.
# Without refinements - global change
class String
def digits
scan(/\d/).map(&:to_i)
end
end
# This affects everything everywhere
"abc123".digits # [1,2,3]
# With refinements - scoped changes
module StringExtensions
refine String do
def digits
scan(/\d/).map(&:to_i)
end
def letters
scan(/[a-zA-Z]/)
end
def alphanumeric
scan(/[a-zA-Z0-9]/)
end
end
refine Integer do
def minutes
self * 60
end
def hours
self * 3600
end
end
end
# Not active yet
"abc123".digits # NoMethodError
# Activate refinements
using StringExtensions
# Now it works in this scope
"abc123".digits # [1,2,3]
class ScopeTest
using StringExtensions
def test
"abc123".digits # Works in instance methods too
end
# But not in class methods unless we use using there too
def self.test
# "abc123".digits # Would fail
end
end
# Refinements are lexical - they only affect the scope
# they're activated in, not called scopes
# Multiple refinements can be stacked
module AllExtensions
refine String do
def bold
"**#{self}**"
end
end
refine Array do
def second
self[1]
end
end
end
using AllExtensions
puts "hello".bold # "**hello**"
puts [1,2,3].second # 2
Exception Handling
Q28: Explain Ruby's exception hierarchy and custom exceptions
A:
# Exception hierarchy (simplified)
# Exception
# ├── NoMemoryError
# ├── ScriptError
# │ ├── NotImplementedError
# │ ├── SyntaxError
# │ └── LoadError
# ├── SecurityError
# ├── SignalException
# ├── StandardError (what you usually rescue)
# │ ├── ArgumentError
# │ ├── IOError
# │ ├── IndexError
# │ ├── NameError
# │ │ └── NoMethodError
# │ ├── RangeError
# │ ├── RegexpError
# │ ├── RuntimeError
# │ ├── TypeError
# │ └── ZeroDivisionError
# └── SystemExit
# Custom exceptions
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
class InvalidCardError < PaymentError; end
class PaymentProcessor
def process(amount, card)
raise ArgumentError, "Amount must be positive" if amount <= 0
raise InvalidCardError, "Card expired" if card.expired?
begin
charge(amount, card)
rescue NetworkError => e
retry_count ||= 0
retry_count += 1
retry if retry_count < 3
raise PaymentError, "Failed after 3 retries: #{e.message}"
rescue InsufficientFundsError => e
# Log and re-raise with more context
raise PaymentError, "Payment failed: #{e.message}"
rescue => e
# Catch any other StandardError
puts "Unexpected error: #{e.class}"
raise # Re-raise
ensure
# Always runs
log_attempt(amount, card)
end
end
private
def charge(amount, card)
# Implementation
end
def log_attempt(amount, card)
# Logging
end
end
# Multiple rescue clauses
begin
risky_operation
rescue ZeroDivisionError
puts "Can't divide by zero"
rescue TypeError, ArgumentError => e
puts "Type or argument error: #{e.message}"
rescue => e
puts "Something else: #{e.class}"
else
# Runs if no exception
puts "All good!"
ensure
# Always runs
puts "Cleanup"
end
# Retry and redo
attempts = 0
begin
attempts += 1
risky_operation
rescue
retry if attempts < 3 # Retry the begin block
end
# Redo - restarts current iteration (not just rescue)
10.times do |i|
puts i
redo if i == 5 && condition # Will print 5 again
end
Q29: What's the difference between rescue, ensure, else, and retry?
A:
def exception_demo
puts "Start"
# begin block (optional in methods)
result = risky_operation
puts "End"
rescue ArgumentError => e
puts "Caught ArgumentError: #{e.message}"
retry if @retries < 3 # Retry from beginning of method
rescue => e
puts "Caught: #{e.class}"
else
# Runs if no exception
puts "Success! Result: #{result}"
return result
ensure
# Always runs, even with return in else
puts "Cleanup"
# ensure's return overrides other returns
# return "override" # Would change return value
end
# Real-world example: File operations
def read_file_safe(filename)
file = File.open(filename)
content = file.read
content.upcase
rescue Errno::ENOENT => e
puts "File not found: #{filename}"
nil
rescue => e
puts "Error reading file: #{e.message}"
nil
else
# Only runs if no exception
puts "Successfully read #{filename}"
content # Return value
ensure
file.close if file # Always close the file
end
# Ensure is often used without rescue
def with_lock
lock.acquire
yield
ensure
lock.release # Always release, even if yield raises
end
Ruby Core Classes Deep Dive
Q30: Explain Enumerable module and when to use it
A:
# Enumerable provides collection methods if you implement #each
class Playlist
include Enumerable
def initialize
@songs = []
end
def add_song(song)
@songs << song
end
# Must implement #each
def each(&block)
@songs.each(&block)
end
# Additional custom methods
def rock_songs
select { |song| song.genre == :rock }
end
def total_duration
map(&:duration).sum
end
end
playlist = Playlist.new
playlist.add_song(Song.new("Bohemian Rhapsody", 355, :rock))
playlist.add_song(Song.new("Sweet Child O' Mine", 235, :rock))
# All Enumerable methods available
playlist.map(&:title) # ["Bohemian Rhapsody", "Sweet Child O' Mine"]
playlist.select(&:rock?) # Filter rock songs
playlist.any? { |s| s.duration > 300 } # true
playlist.max_by(&:duration) # Longest song
# Lazy enumeration for large collections
(1..Float::INFINITY).lazy
.select(&:even?)
.map { |x| x * 2 }
.first(5) # [4, 8, 12, 16, 20]
Q31: What's the difference between Array, Set, and SortedSet?
A:
require 'set'
# Array - ordered, allows duplicates
array = [3, 1, 2, 1, 3]
array.uniq # [3, 1, 2] (temporary)
array[0] # 3 (indexed access)
# Set - unordered, no duplicates
set = Set.new([3, 1, 2, 1, 3]) # <Set: {3, 1, 2}>
set.add(1) # No change (already exists)
set.include?(2) # true (fast lookup)
# SortedSet - ordered, no duplicates
sorted = SortedSet.new([3, 1, 2, 1, 3]) # <SortedSet: {1, 2, 3}>
sorted.to_a # [1, 2, 3]
# Performance comparison
require 'benchmark'
array = (1..100000).to_a.shuffle
set = array.to_set
Benchmark.bm do |x|
x.report("Array include?") { 1000.times { array.include?(50000) } }
x.report("Set include? ") { 1000.times { set.include?(50000) } }
x.report("Array uniq ") { array.uniq }
x.report("Set from array ") { array.to_set }
end
# Set operations
a = [1, 2, 3].to_set
b = [2, 3, 4].to_set
a | b # Union: <Set: {1, 2, 3, 4}>
a & b # Intersection: <Set: {2, 3}>
a - b # Difference: <Set: {1}>
a ^ b # Symmetric difference: <Set: {1, 4}>
Q32: Explain Struct and OpenStruct differences
A:
# Struct - lightweight class, faster
Person = Struct.new(:name, :age, :email) do
def adult?
age >= 18
end
def to_s
"#{name} (#{age})"
end
end
person = Person.new("John", 30, "john@example.com")
person.name # "John"
person.age = 31 # Can modify
person[:name] # Hash-like access "John"
person.to_h # {:name=>"John", :age=>31, :email=>"john@example.com"}
# OpenStruct - flexible but slower
require 'ostruct'
os = OpenStruct.new(name: "John", age: 30)
os.name # "John"
os.email = "john@example.com" # Can add new attributes
# Performance comparison
require 'benchmark'
struct = Person.new("John", 30)
ostruct = OpenStruct.new(name: "John", age: 30)
Benchmark.bm do |x|
x.report("Struct read: ") { 1000000.times { struct.name } }
x.report("OpenStruct read:") { 1000000.times { ostruct.name } }
x.report("Struct write: ") { 1000000.times { struct.age = 31 } }
x.report("OpenStruct write:") { 1000000.times { ostruct.age = 31 } }
end
# When to use:
# - Struct: Known attributes, performance critical, need lightweight class
# - OpenStruct: Dynamic attributes, prototyping, configuration
Performance and Optimization
Q33: How do you profile Ruby code?
A:
# 1. Benchmark module
require 'benchmark'
time = Benchmark.measure do
# code to measure
1000000.times { "string".upcase }
end
puts time # real time, user time, system time
# Multiple benchmarks
Benchmark.bm(20) do |x|
x.report("String#upcase:") { 1000000.times { "string".upcase } }
x.report("String#upcase!:") { 1000000.times { "string".upcase! } }
end
# 2. Ruby Profiler
require 'profile'
def slow_method
array = []
(1..10000).each do |i|
array << i.to_s
end
array.join
end
slow_method # Profile output shows where time is spent
# 3. Memory profiling
require 'memory_profiler'
report = MemoryProfiler.report do
# code to profile
array = (1..100000).to_a
array.map(&:to_s)
end
report.pretty_print
# 4. StackProf for CPU profiling
require 'stackprof'
StackProf.run(mode: :cpu, out: 'stackprof.dump') do
# code to profile
end
# 5. Custom profiling
class Profiler
def self.profile(name, &block)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = block.call
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
puts "#{name} took #{duration.round(4)}s"
result
end
def self.memory(&block)
GC.start
before = GC.stat(:total_allocated_objects)
result = block.call
GC.start
after = GC.stat(:total_allocated_objects)
puts "Allocated: #{after - before} objects"
result
end
end
Profiler.profile("Slow operation") do
sleep 1
end
Q34: What are common Ruby performance pitfalls and solutions?
A:
# Pitfall 1: Unnecessary object creation
# Bad
def process(data)
data.map { |item| item.to_s.upcase.strip }
end
# Good - reuse objects
def process(data)
data.map! do |item|
item = item.to_s
item.upcase!
item.strip!
item
end
end
# Pitfall 2: Inefficient string concatenation
# Bad
html = ""
1000.times { |i| html += "<p>#{i}</p>" } # Creates new string each time
# Good
html = []
1000.times { |i| html << "<p>#{i}</p>" }
html = html.join
# Better for large strings
html = String.new
1000.times { |i| html << "<p>#{i}</p>" }
# Pitfall 3: N+1 queries in Rails
# Bad
posts.each { |post| puts post.comments.count }
# Good
posts.includes(:comments).each { |post| puts post.comments.count }
# Pitfall 4: Inefficient data structures
# Bad - Array for frequent lookups
users = User.all.to_a
users.find { |u| u.id == id } # O(n)
# Good - Hash for lookups
users = User.all.index_by(&:id) # O(1)
users[id]
# Pitfall 5: Heavy computations in loops
# Bad
array.each do |item|
result = expensive_computation # Same computation each iteration
process(item, result)
end
# Good
result = expensive_computation # Compute once
array.each { |item| process(item, result) }
# Pitfall 6: Not using lazy enumeration for large datasets
# Bad
(1..Float::INFINITY).select(&:even?).first(10) # Infinite loop!
# Good
(1..Float::INFINITY).lazy.select(&:even?).first(10)
# Pitfall 7: Inefficient regular expressions
# Bad
text =~ /.*pattern.*/ # Greedy, slow
# Good
text =~ /pattern/ # No need for .* unless matching entire string
Concurrency and Parallelism
Q35: Explain Threads, Fibers, and Ractors in Ruby
A:
# Threads - preemptive, shared memory
threads = []
5.times do |i|
threads << Thread.new(i) do |num|
puts "Thread #{num} starting"
sleep rand(0.1..0.5)
puts "Thread #{num} done"
end
end
threads.each(&:join) # Wait for all threads
# Thread safety issues
@counter = 0
threads = 10.times.map do
Thread.new do
1000.times { @counter += 1 } # Race condition!
end
end
threads.each(&:join)
puts @counter # Probably not 10000
# Solution: Mutex
@counter = 0
@mutex = Mutex.new
threads = 10.times.map do
Thread.new do
1000.times do
@mutex.synchronize { @counter += 1 }
end
end
end
threads.each(&:join)
puts @counter # 10000
# Fibers - cooperative, lightweight
fiber = Fiber.new do
puts "Fiber: step 1"
Fiber.yield
puts "Fiber: step 2"
"result"
end
puts "Main: before"
fiber.resume
puts "Main: between"
result = fiber.resume
puts "Main: result = #{result}"
# Ractors (Ruby 3+) - parallel, isolated memory
ractor = Ractor.new do
result = 0
10.times { result += 1 }
result
end
puts ractor.take # 10
# Ractors communicate via message passing
producer = Ractor.new do
Ractor.yield "data from producer"
end
consumer = Ractor.new(producer) do |p|
data = p.take
"Consumer got: #{data}"
end
puts consumer.take # "Consumer got: data from producer"
Q36: What's the Global Interpreter Lock (GIL) and how does it affect Ruby?
A:
# MRI Ruby has GIL - only one thread executes Ruby code at a time
# I/O operations release GIL
require 'benchmark'
# CPU-bound - GIL prevents parallelism
def cpu_bound_task
result = 0
10_000_000.times { result += 1 }
result
end
Benchmark.bm do |x|
x.report("Single thread:") { cpu_bound_task }
x.report("Multiple threads:") do
threads = 5.times.map { Thread.new { cpu_bound_task } }
threads.each(&:join)
end
# Multiple threads ≈ same time due to GIL
end
# I/O-bound - GIL releases during I/O
def io_bound_task
sleep 0.1 # Simulate I/O
# GIL is released during sleep
end
Benchmark.bm do |x|
x.report("Sequential I/O:") { 5.times { io_bound_task } }
x.report("Concurrent I/O:") do
threads = 5.times.map { Thread.new { io_bound_task } }
threads.each(&:join)
end
# Concurrent is ~5x faster
end
# GIL implications:
# 1. Threads great for I/O-bound work
# 2. For CPU-bound, use multiple processes or Ractors
# 3. Thread safety still matters (GIL doesn't protect everything)
# Multiple processes for CPU-bound
require 'parallel'
# Parallel gem
results = Parallel.map(1..5, in_processes: 5) do |i|
cpu_bound_task
end
Advanced Practice Problems
Q37: Implement your own attr_accessor using metaprogramming
A:
class Module
def my_attr_accessor(*names)
names.each do |name|
# Getter
define_method(name) do
instance_variable_get("@#{name}")
end
# Setter
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
end
# Optional: Add validation hook
define_method("#{name}_was") do
@previous_values ||= {}
@previous_values[name]
end
# Override setter to track changes
original_setter = instance_method("#{name}=")
define_method("#{name}=") do |value|
@previous_values ||= {}
@previous_values[name] = instance_variable_get("@#{name}")
original_setter.bind(self).call(value)
end
end
end
def my_attr_reader(*names)
names.each do |name|
define_method(name) { instance_variable_get("@#{name}") }
end
end
def my_attr_writer(*names)
names.each do |name|
define_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
end
end
end
class Person
my_attr_accessor :name, :age
my_attr_reader :id
def initialize(name, age)
@name = name
@age = age
@id = rand(1000)
end
end
p = Person.new("John", 30)
puts p.name # "John"
p.name = "Jane"
puts p.name_was # "John"
Q38: Create a simple DSL for routing (like Sinatra)
A:
class Router
class << self
attr_reader :routes
def draw(&block)
@routes = {GET: {}, POST: {}, PUT: {}, DELETE: {}}
instance_eval(&block)
end
def get(path, to: nil, &block)
route(:GET, path, to, &block)
end
def post(path, to: nil, &block)
route(:POST, path, to, &block)
end
def put(path, to: nil, &block)
route(:PUT, path, to, &block)
end
def delete(path, to: nil, &block)
route(:DELETE, path, to, &block)
end
def route(method, path, to, &block)
handler = block || to
@routes[method][path] = handler
end
def call(env)
method = env[:method].to_s.upcase.to_sym
path = env[:path]
if handler = @routes[method][path]
if handler.is_a?(Proc)
handler.call(env)
else
controller, action = handler.to_s.split('#')
Object.const_get(controller.capitalize).new.send(action, env)
end
else
[404, {'Content-Type' => 'text/plain'}, ['Not Found']]
end
end
end
end
class Application
def self.routes(&block)
Router.draw(&block)
end
def call(env)
Router.call(env)
end
end
# Usage
Application.routes do
get "/" do |env|
[200, {'Content-Type' => 'text/html'}, ['<h1>Home</h1>']]
end
get "/about", to: "pages#about"
post "/users" do |env|
[201, {'Content-Type' => 'text/plain'}, ['User created']]
end
end
class Pages
def about(env)
[200, {'Content-Type' => 'text/html'}, ['<h1>About Us</h1>']]
end
end
# Test
app = Application.new
puts app.call({method: "GET", path: "/"})
puts app.call({method: "GET", path: "/about"})
Q39: Implement a thread-safe connection pool
A:
class ConnectionPool
class TimeoutError < StandardError; end
def initialize(size: 5, timeout: 5, &block)
@size = size
@timeout = timeout
@block = block
@connections = []
@mutex = Mutex.new
@resource = ConditionVariable.new
@available = size
end
def with_connection
conn = acquire_connection
yield conn
ensure
release_connection(conn) if conn
end
private
def acquire_connection
@mutex.synchronize do
@resource.wait(@mutex, @timeout) while @available <= 0
@available -= 1
@connections.pop || create_connection
end
rescue ThreadError
raise TimeoutError, "Couldn't acquire connection within #{@timeout}s"
end
def release_connection(conn)
@mutex.synchronize do
@connections << conn
@available += 1
@resource.signal
end
end
def create_connection
@block.call
end
end
# Usage
pool = ConnectionPool.new(size: 3) do
{id: rand(1000)} # Simulated connection
end
threads = 10.times.map do |i|
Thread.new do
begin
pool.with_connection do |conn|
puts "Thread #{i} got connection #{conn[:id]}"
sleep rand(0.1..0.3) # Simulate work
puts "Thread #{i} releasing connection"
end
rescue ConnectionPool::TimeoutError
puts "Thread #{i} timed out waiting for connection"
end
end
end
threads.each(&:join)
Q40: Implement a caching decorator with expiration
A:
class Cache
class Entry
attr_reader :value, :expires_at
def initialize(value, ttl)
@value = value
@expires_at = Time.now + ttl if ttl
end
def expired?
@expires_at && Time.now > @expires_at
end
end
def initialize
@store = {}
@mutex = Mutex.new
@stats = {hits: 0, misses: 0}
end
def fetch(key, ttl: nil, &block)
cleanup if rand < 0.01 # Probabilistic cleanup
@mutex.synchronize do
if (entry = @store[key]) && !entry.expired?
@stats[:hits] += 1
return entry.value
end
@stats[:misses] += 1
value = block.call
@store[key] = Entry.new(value, ttl)
value
end
end
def delete(key)
@mutex.synchronize { @store.delete(key) }
end
def clear
@mutex.synchronize { @store.clear }
end
def stats
@mutex.synchronize { @stats.dup }
end
private
def cleanup
@mutex.synchronize do
@store.delete_if { |_, entry| entry.expired? }
end
end
end
# Decorator for expensive methods
module Cacheable
def cacheable(method_name, ttl: 60)
original_method = instance_method(method_name)
cache = Cache.new
define_method(method_name) do |*args, **kwargs|
key = "#{method_name}_#{args.hash}_#{kwargs.hash}"
cache.fetch(key, ttl: ttl) do
original_method.bind(self).call(*args, **kwargs)
end
end
# Add cache clearing method
define_method("clear_#{method_name}_cache") do
cache.clear
end
end
end
class ExpensiveCalculator
extend Cacheable
cacheable :fibonacci, ttl: 300
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
end
calc = ExpensiveCalculator.new
puts calc.fibonacci(40) # Slow first time
puts calc.fibonacci(40) # Fast from cache
Ruby Object Model
Q1: Explain Ruby's Object Model in detail
A: Ruby's object model is a fundamental concept that defines how objects, classes, and modules relate to each other.
Theoretical Foundation:
Ruby follows a "class-based object model" where everything is an object, and every object has a class. However, unlike traditional class-based languages, Ruby implements a single-inheritance model with mixins and has a unique metaclass system.
Key Principles:
-
Objects and Classes: Every object is an instance of a class. Classes themselves are objects (instances of Class).
-
The Class Hierarchy: All classes ultimately inherit from BasicObject → Object → Kernel (module) → YourClass.
-
Metaclasses (Eigenclasses): Every object has a hidden class called its metaclass (or eigenclass, or singleton class) that stores object-specific methods.
-
The Flozen Hierarchy:
BasicObject (root)
↑
Object (includes Kernel)
↑
YourClass
↑
instances
The Metaclass Concept:
Every object in Ruby has two classes:
- The regular class (what you get with
.class) - The metaclass (hidden class for singleton methods)
# Conceptual representation (not actual code)
obj = Object.new
# The object's structure:
obj (instance)
├── points to its regular class → Object
└── has a hidden metaclass → #<Class:#<Object:0x...>>
└── stores obj's singleton methods
Class Objects:
When you define a class, you're actually creating an object:
- The class name becomes a constant pointing to a Class object
- This Class object has its own metaclass for class methods
- Class methods are actually singleton methods on the class object
The Module Inclusion Mechanism:
When you include a module, Ruby creates an anonymous proxy class (inclusion class) that wraps the module and inserts it into the ancestor chain.
Q2: Explain the difference between classes and objects in terms of memory and behavior
A:
Theoretical Distinction:
-
Classes as Blueprints vs Objects as Instances:
- Classes define behavior (methods) but don't have state until instantiated
- Objects have state (instance variables) and inherit behavior from their class
-
Memory Allocation:
- Classes: Stored once in memory, contain method tables and constant references
- Objects: Each gets its own memory for instance variables, but share method tables with their class
-
Method Storage:
- Methods are stored in the class, not in individual objects
- Objects contain a pointer to their class (the
klasspointer) - This is why 1000 objects of the same class use much less memory than 1000 objects with singleton methods
-
The
klassPointer:- Every object has an internal
klasspointer that references its class - When you call a method, Ruby follows this pointer to find the method
- Every object has an internal
-
Singleton Classes:
- When you define a method on a single object, Ruby creates a singleton class
- The object's
klasspointer is updated to point to this singleton class - The singleton class's superclass is the object's original class
Q3: What is the difference between class and Class?
A:
Theoretical Explanation:
This is one of the most confusing concepts in Ruby: the difference between class (keyword) and Class (constant/class object).
-
class(keyword):- A language construct for defining classes
- Creates a new scope and sets
selfto the class being defined - Example:
class Person; end
-
Class(class object):- The actual class object that represents all classes
Classis an instance ofClass(yes, it's self-referential)- All classes are instances of
Class
The Chicken-and-Egg Problem:
How can Class be an instance of itself? This is resolved at the C level in the Ruby interpreter:
BasicObjectis the root of the class hierarchyClassinherits fromModule, which inherits fromObject, which inherits fromBasicObject- But
BasicObject.classreturnsClass - This circular reference is hardcoded in the interpreter
The Class-Module Relationship:
Classis a subclass ofModule- This is why classes can do everything modules can (include, extend, etc.) plus instantiation
Q4: Explain the concept of "open classes" and its implications
A:
Theoretical Foundation:
Open classes mean that class definitions in Ruby are not closed; you can reopen any class at any time to add or modify methods.
How It Works Internally:
When Ruby encounters a class keyword:
- If the class doesn't exist, it creates a new class object
- If the class already exists, it reopens the existing class
- Any methods defined inside are added to the existing class
Implications:
- Monkey Patching: You can modify built-in classes
- Refinements: A way to scope monkey patches (introduced in Ruby 2.0)
- Method Racing: Later definitions override earlier ones
- Load Order Matters: The last loaded definition wins
Theoretical Problems:
- Global Impact: Changes affect all code, including gems
- Naming Conflicts: Different libraries might patch the same method differently
- Debugging Difficulty: It's not obvious where a method came from
- Version Compatibility: Your patches might break with new Ruby versions
The Solution - Refinements (Theoretical):
Refinements provide lexical scoping for monkey patches:
- They're active only in specific scopes
- They don't pollute the global namespace
- They're resolved at compile time, not runtime
Method Lookup Path
Q5: Explain the complete method lookup path in Ruby
A:
Theoretical Model:
When you call a method on an object, Ruby follows this exact path:
1. Look in the object's singleton class (eigenclass)
2. Look in the singleton class's included modules (in reverse inclusion order)
3. Look in the object's class
4. Look in the class's included modules (in reverse inclusion order)
5. Look in the superclass
6. Repeat steps 4-5 up to BasicObject
7. If not found, start again at step 1 looking for method_missing
8. If still not found, call method_missing on the original object
The Ancestor Chain:
Every class has an ancestor chain visible via .ancestors. This chain includes:
- The class itself
- Included modules (as proxy classes)
- The superclass
- Modules included in superclasses
Module Inclusion Mechanics:
When you include a module:
- Ruby creates an anonymous class (inclusion class) that wraps the module
- This anonymous class is inserted into the ancestor chain right above the including class
- The anonymous class's superclass is set to the including class's superclass
When you prepend a module:
- Similar anonymous class is created
- But it's inserted below the including class (so it gets looked at first)
- The prepended class's superclass becomes the including class
The super Keyword:
super looks up the method in the next class/module in the ancestor chain:
- Without arguments: passes all original arguments
- With empty parentheses: passes no arguments
- With specific arguments: passes those arguments
Q6: How does Ruby decide which method to call when multiple modules define the same method?
A:
Theoretical Explanation:
This involves understanding Ruby's method resolution order (MRO) and how modules are inserted into the ancestor chain.
Inclusion Order Rules:
- Last included wins (for method definition)
- First in ancestor chain wins (for method lookup)
When you include multiple modules:
- The last module included is checked first in the ancestor chain
- This is because modules are inserted above the class but below previously included modules
Example Theory:
class C
include M1 # M1 inserted above C
include M2 # M2 inserted above M1
end
# Ancestors: [C, M2, M1, Object]
The Diamond Problem in Ruby:
Ruby doesn't have multiple inheritance, so it avoids the classic diamond problem. However, with modules, you can have:
Module A
/ \
Module B Module C
\ /
Class D (includes B and C)
If both B and C define the same method, Ruby's linearization (based on inclusion order) determines which one is called.
Q7: Explain include, prepend, and extend in terms of the ancestor chain
A:
Theoretical Model:
These three methods manipulate the ancestor chain in different ways:
1. include (Adds after the class in the chain):
- Creates an anonymous proxy class for the module
- Inserts this proxy above the including class
- The proxy's superclass becomes the class's original superclass
- The class's superclass becomes the proxy
2. prepend (Adds before the class in the chain):
- Creates an anonymous proxy class for the module
- Inserts this proxy below the including class
- The proxy's superclass becomes the class
- The class's superclass remains unchanged
3. extend (Adds to the singleton class):
- Adds module methods to the object's singleton class
- For a class object, this adds class methods
- The module becomes an ancestor of the singleton class
Visual Representation:
Without modules:
[Class] → [Superclass] → [Object] → [BasicObject]
With include:
[Class] → [Module Proxy] → [Superclass] → [Object]
With prepend:
[Module Proxy] → [Class] → [Superclass] → [Object]
Q8: What happens when you call super in a method that was included from a module?
A:
Theoretical Explanation:
super always looks for the next method in the ancestor chain, regardless of where the current method came from.
The Call Chain:
When you call super in a module method:
- Ruby looks at the current class/module's position in the ancestor chain
- It finds the next entity in the chain (could be another module or a class)
- It looks for the same method name in that entity
- If found, it calls it; if not, it continues up the chain
Important Rules:
superdoesn't know or care that it was called from a module- It simply looks at the static ancestor chain
- This is determined at compile time, not runtime
The Superclass Chain vs. Module Chain:
- Modules don't have superclasses in the traditional sense
- Their "next" in the chain is determined by their position in the including class's ancestors
Variable Scope and Binding
Q9: Explain the different variable scopes and their lifetimes
A:
Theoretical Framework:
Ruby has four main variable scopes, each with different lifetimes and visibility rules:
1. Local Variables:
- Scope: Current scope (method, block, or file)
- Lifetime: From creation until the end of the current scope
- Visibility: Only within the defining scope
- Creation: When Ruby sees an assignment, not when the name is mentioned
- Parser Behavior: Ruby's parser decides whether a name is a local variable or a method call based on what it's seen before
2. Instance Variables (@var):
- Scope: Specific to one object instance
- Lifetime: As long as the object exists
- Visibility: Within all instance methods of the object
- Default Value:
nilif accessed before assignment (no error) - Storage: Stored in the object's
RBasicstructure in C
3. Class Variables (@@var):
- Scope: A class and all its instances, plus all subclasses
- Lifetime: As long as the class exists (typically program lifetime)
- Visibility: Within class methods, instance methods, and subclasses
- Problems: Shared across inheritance hierarchy - can lead to unexpected behavior
- Warning: Considered problematic; class instance variables are often preferred
4. Global Variables ($var):
- Scope: Entire program, across all classes and methods
- Lifetime: Program lifetime
- Visibility: Everywhere
- Built-in Globals: Many with special meanings (
$!,$@,$~,$&, etc.) - Problems: Create hidden dependencies, make code hard to reason about
5. Constants:
- Scope: Lexical scope (where defined) plus inheritance
- Lifetime: Program lifetime
- Lookup: Ruby searches lexical scope first, then inheritance hierarchy
- Modification: Can be changed (warning only), but shouldn't be
Q10: What is a closure in Ruby and how does variable binding work?
A:
Theoretical Definition:
A closure is a function (or block/proc/lambda) that captures and remembers the environment (local variables, self, etc.) where it was created.
The Binding Concept:
When a closure is created, Ruby takes a "snapshot" of the current execution context:
- All local variables in scope
- The current value of
self - The current class/module context
- Any blocks that were passed
This snapshot is called a binding and is stored with the proc object.
How Binding Works Internally:
In the Ruby source code (C level):
- When a block is created, Ruby allocates a
rb_binding_tstructure - This structure holds pointers to the current execution context
- It increments reference counts on captured variables
- When the proc is called, it restores this context
Variable Lifetime in Closures:
Variables captured by a closure outlive their normal scope:
def make_counter
count = 0 # count would normally die here
return Proc.new { count += 1 } # but it's captured
end
counter = make_counter
counter.call # 1 - count still exists!
Why This Matters:
- Closures prevent garbage collection of captured variables
- Can cause memory leaks if not careful
- Each closure call maintains its own copy of captured variables
Q11: Explain the difference between lexical scope and dynamic scope
A:
Theoretical Distinction:
Ruby uses lexical (static) scope for most things, but has elements of dynamic scope in some areas.
Lexical Scope (Static):
- Based on where code is written, not how it's called
- Determined at compile time
- Used for: local variables, constants,
selfin some contexts - Example: Constants look up the lexical nesting first
Dynamic Scope:
- Based on the call stack at runtime
- Determined at runtime
- Used for:
self,method_missing, some special globals - Example:
selfchanges based on what object called the method
Lexical Scope Example:
module A
VALUE = 1
class B
VALUE = 2
def test
VALUE # Lexical: looks in A::B first, not based on caller
end
end
end
Dynamic Scope Example:
def current_user
@current_user # This is dynamic - depends on what @current_user is in the calling object
end
The binding Method:
binding captures the current lexical scope and returns a Binding object that can be passed around:
def get_binding
x = 10
binding # Captures x=10, self, etc.
end
b = get_binding
b.eval("x") # 10 - even though x is out of lexical scope
Q12: How does Ruby resolve constants and what is the constant lookup path?
A:
Theoretical Framework:
Constant lookup in Ruby is complex because it combines lexical scoping with inheritance.
The Lookup Algorithm:
When Ruby encounters a constant reference (like VALUE), it searches:
- Lexical Scope (current nesting): Starting from the innermost nesting and moving outward
- Inheritance Hierarchy: If not found lexically, look up the superclass chain
- Top Level: Finally, look at
Object(and thereforeKernelandBasicObject)
The Module.nesting Method:
This shows the current lexical nesting path:
module A
module B
def self.nesting_example
puts Module.nesting.inspect # [A::B, A]
end
end
end
Explicit Scope Resolution:
::VALUE- start lookup at top levelA::VALUE- start in module Aself.class::VALUE- use the class of the current object
Constant Assignment:
- When you assign a constant, Ruby creates it in the current lexical scope
- This can lead to "constant stealing" if not careful
Autoloading:
Ruby's autoload mechanism leverages constant missing:
- When a constant isn't found,
const_missingcan be called - Rails' autoloader uses this to load files on demand
- This is why Rails constants appear to "magically" appear
Ruby's Execution Context
Q13: Explain self in all possible contexts
A:
Theoretical Definition:
self is the current object or default receiver in Ruby. It's the object that receives messages when no explicit receiver is specified.
Self in Different Contexts:
1. At the Top Level (outside any class):
selfismain(an instance ofObject)- This is the "main object" created by the Ruby interpreter
- Methods defined here become private instance methods of
Object
2. Inside a Class Definition (but outside methods):
selfis the class object itself- Example:
class Person; self endreturnsPerson - This is why class methods are defined as
def self.method
3. Inside an Instance Method:
selfis the instance that received the method call- Changes based on who called the method
- Always refers to the receiver object
4. Inside a Class Method:
selfis the class object- Even though it's defined on the class, within the method it's the class
5. Inside a Module:
- In module definition:
selfis the module - In module instance methods:
selfwill be the including object
6. Inside a Block:
- Blocks inherit the
selffrom where they're defined - This is why blocks are closures - they capture
self - Procs also capture
selffrom creation context
7. Inside a Lambda:
- Same as blocks - captures
selffrom creation context - Unlike blocks, lambdas check arity
8. Inside a Singleton Method (def obj.method):
selfis the objectobj- This is how singleton methods know which object they're on
The self Switch:
Ruby provides ways to change self:
instance_eval: changesselfto the receiverclass_eval: changesselfto the class
Q14: What is the difference between class_eval and instance_eval in terms of self?
A:
Theoretical Distinction:
The key difference is what self becomes during evaluation:
instance_eval:
- Changes
selfto the receiver object - Methods defined become singleton methods of that object
- Instance variables accessed are those of the receiver
- Can access private methods of the receiver
class_eval (alias module_eval):
- Changes
selfto the class/module - Methods defined become instance methods of the class
- Instance variables accessed are those of the class
- Behaves like you're inside a class definition
Theoretical Implications:
When you call instance_eval on a class:
selfbecomes the class- Methods defined become class methods (singleton methods of the class)
- This is counterintuitive but consistent with the model
When to Use Which:
-
Use
instance_evalwhen you want to:- Access an object's internal state
- Define singleton methods
- Create DSLs (like
configureblocks)
-
Use
class_evalwhen you want to:- Define instance methods
- Add to class-level state
- Open a class when you don't have the name
Q15: Explain the concept of "current class" in Ruby
A:
Theoretical Framework:
Ruby tracks a current class (or module) context during execution. This is where methods defined with def (without an explicit receiver) will be added.
How Current Class is Determined:
-
At the top level: There is no current class; methods become private methods of
Object -
Inside a class definition: The current class is that class
-
Inside a method: The current class is the class of the object (but
definside a method creates a method on the containing class? Actually, this is tricky) -
Inside
class_eval: The current class becomes the receiver -
Inside
instance_eval: The current class becomes the singleton class of the receiver
The class Keyword Effect:
class Person; endsets the current class toPerson- After
end, it reverts to the previous current class
The def self.method Mystery:
When you write def self.method:
- Ruby evaluates
self(which might be a class or object) - It gets that object's singleton class
- It makes that singleton class the current class for the method definition
- The method is defined in that singleton class
Nested Class Definitions:
class Outer
# current class: Outer
class Inner
# current class: Inner
end
# current class: Outer again
end
Memory Management Deep Dive
Q16: Explain Ruby's memory model in detail
A:
Theoretical Overview:
Ruby's memory model is a combination of:
- Managed Heap for Ruby objects
- System Stack for method calls and local variables
- C Heap for internal C structures
The Ruby Heap Structure:
The Ruby heap is divided into pages (typically 16KB each). Each page contains:
- A header with metadata
- Multiple slots (each can hold one Ruby object)
- A bitmap for tracking free slots
Object Allocation:
When you create a new object:
- Ruby finds a free slot in an existing heap page
- If no free slots, allocate a new page
- If no more pages, trigger garbage collection
- If still no space, allocate more memory from OS
Object Layout (RBasic structure):
Every Ruby object has a common header (RBasic in C) containing:
- flags: Type information, GC flags, etc. (usually 1 word)
- klass: Pointer to the object's class (1 word)
- Then object-specific data (instance variables, etc.)
Memory Pools:
Ruby maintains different pools for different object sizes:
- RVALUE pool: 40 bytes (typical object)
- Larger objects: Allocated separately, pointed to by RVALUE
Variable Storage:
- Local variables: On the stack (fast)
- Instance variables: In the object's
RBasicplus extension if needed - Class variables: In the class object's instance variable table
- Global variables: In a global table
Q17: How does Ruby's garbage collector work at a theoretical level?
A:
Theoretical Framework:
Ruby uses a mark-and-sweep garbage collector with generational optimization.
Core Concepts:
1. Mark Phase:
- Start from "root" objects (globals, stack variables, registers)
- Traverse object graph, marking reachable objects
- Uses a mark stack to avoid recursion
- Objects not marked are considered garbage
2. Sweep Phase:
- Iterate through all heap pages
- For unmarked objects, add their slots to free list
- Reset mark bits for marked objects (for next cycle)
Generational GC (Ruby 2.1+):
Objects are divided into generations:
- Young generation: New objects (collected frequently)
- Old generation: Objects that survived multiple collections (collected rarely)
The Write Barrier: When an old object references a new object, Ruby must record this:
- Otherwise, young GC might miss the reference
- Write barrier tracks these "cross-generation" references
Incremental Marking (Ruby 2.2+):
- Marking can pause the program
- Incremental marking spreads marking over time
- Uses tri-color marking (white, gray, black)
Tri-Color Marking:
- White: Not marked (candidate for collection)
- Gray: Marked, but not all children marked yet
- Black: Fully marked, no more traversal needed
The GC Process:
-
Minor GC: Young generation collection
- Fast, frequent
- Promotes surviving objects to old generation
-
Major GC: Full collection (both generations)
- Slow, rare
- Collects everything
Lazy Sweeping (Ruby 2.0+):
- Don't sweep all at once
- Sweep incrementally as new allocations happen
- Reduces GC pause times
Q18: Explain memory leaks in Ruby and how to identify them
A:
Theoretical Causes:
Memory leaks in Ruby occur when objects remain reachable (thus not collectable) even though they're no longer needed.
Common Leak Patterns:
1. Global Caches:
- Caches without eviction policies
- Data stored in class variables or global variables
- Objects referenced from constants
2. Callback Accumulation:
- Adding to arrays/hashes without removal
- Observer patterns without cleanup
- Event listeners that persist
3. Closure Captures:
- Blocks that capture large objects
- The captured objects can't be GC'd while the block exists
- Especially problematic with long-lived procs
4. Thread Local Variables:
- Data stored in thread-local storage
- Threads might be pooled/reused
- Old data persists across operations
5. C Extensions:
- Manual memory allocation in C
- Ruby can't track C-allocated memory
- Must be manually freed
6. Cyclic References:
- Ruby can handle most cycles with GC
- But cycles involving C structures or finalizers might leak
Detection Methods:
1. ObjectSpace Analysis:
# Count objects by class
ObjectSpace.each_object.to_a.group_by(&:class).map { |k,v| [k, v.size] }
# Track object growth over time
GC.start
before = ObjectSpace.each_object.size
# operation
GC.start
after = ObjectSpace.each_object.size
puts "Growth: #{after - before}"
2. Memory Profiling Gems:
memory_profiler: Reports allocations and retained objectsderailed_benchmarks: For Rails memory analysisheapy: Analyzes heap dumps
3. Heap Dumps:
require 'objspace'
ObjectSpace.dump_all(output: File.open('heap.json', 'w'))
4. GC Statistics:
GC.stat # Shows heap usage, counts, etc.
GC.latest_gc_info # Info about last GC
Q19: What is object fragmentation and how does Ruby handle it?
A:
Theoretical Definition:
Object fragmentation occurs when free memory is broken into small, non-contiguous chunks, making it impossible to allocate large objects even though total free memory is sufficient.
Types of Fragmentation:
1. Internal Fragmentation:
- Object slots are fixed size (40 bytes typically)
- Small objects waste space in large slots
- Objects larger than slot size need separate allocation
2. External Fragmentation:
- Free slots scattered across heap pages
- Can't allocate contiguous space for large objects
- Common after many allocations/deallocations
Ruby's Approach:
Slot-based Allocation:
- All objects fit in standard slots (40 bytes on 64-bit)
- Larger objects (strings, arrays) allocate separate memory
- The slot just holds a pointer to external memory
Compaction (Ruby 2.7+): Ruby can now compact the heap:
GC.compact # Defragment the heap
How Compaction Works:
- Move objects to consolidate free space
- Update all references to moved objects
- Requires write barriers and careful coordination
- Can be done at runtime (Ruby 3.0+)
Heap Sorting (Ruby 3.1+):
- Objects can be sorted by age
- Improves memory locality
- Better cache performance
Q20: Explain copy-on-write (COW) and how Ruby supports it
A:
Theoretical Concept:
Copy-on-Write is a memory optimization where multiple processes share the same memory pages until one tries to modify it, at which point a copy is made.
COW in Forking:
When a process forks:
- Child process gets copy of parent's memory
- With COW, they initially share physical pages
- Only when either writes, the page is copied
Ruby's COW Challenges:
Ruby's GC marks objects during collection, which writes to pages, breaking COW:
- Each GC cycle would cause page copies
- Memory usage balloons with multiple forked processes
The Solution - Bitmap Marking (Ruby 2.0+):
Instead of marking objects directly:
- GC marks are stored in a separate bitmap (not in object pages)
- Object pages remain read-only during GC
- Forked processes can share pages longer
- Significant memory savings for forked servers (like Unicorn)
Implementation Details:
- Each heap page has a corresponding bitmap page
- Mark bits stored in bitmap, not in objects
- Write barrier still needed for old-to-young references
- Trade-off: slightly slower marking for better COW
Parsing and Compilation
Q21: Explain the stages of Ruby code execution
A:
Theoretical Pipeline:
Ruby code goes through several stages before execution:
Stage 1: Lexical Analysis (Scanning)
- Source code → tokens
- Reads characters, produces tokens
- Handles encoding, comments, whitespace
- Built with
lex.cin MRI
Stage 2: Syntactic Analysis (Parsing)
- Tokens → Abstract Syntax Tree (AST)
- Checks syntax rules
- Produces node tree
- Uses LALR parser (yacc/bison grammar)
Stage 3: Compilation
- AST → YARV Instructions (bytecode)
- Optimizations at this stage
- Creates
iseq(instruction sequence) objects
Stage 4: Execution
- YARV Virtual Machine runs bytecode
- Stack-based VM
- Method lookup, variable access, etc.
Ruby 1.8 vs 1.9+ Difference:
- 1.8: AST interpreter (slower)
- 1.9+: Bytecode compiler (faster)
Just-In-Time (JIT) Compilation (Ruby 3.0+):
- MJIT: Method-based JIT compiler
- YJIT: Newer, faster JIT (Ruby 3.1+)
- Compiles hot paths to machine code
Q22: What is YARV and how does it work?
A:
Theoretical Definition:
YARV (Yet Another Ruby VM) is the bytecode interpreter that executes Ruby code in MRI 1.9+.
YARV Architecture:
Ruby Source → Parser → AST → Compiler → YARV Bytecode → VM → Execution
Bytecode Instructions:
YARV uses a stack-based VM with instructions like:
putstring"hello" # Push string onto stacksend:puts, 1 # Call method with 1 argumentleave# Return from method
Instruction Structure: Each instruction has:
- Opcode (operation code)
- Operands (data for the operation)
- Line number (for debugging)
The Execution Stack:
YARV maintains:
- Evaluation stack: For expression results
- Call stack: For method calls and returns
- PC (Program Counter): Current instruction
- CFP (Control Frame Pointer): Current stack frame
Stack Frames:
Each method call creates a frame containing:
- Local variables
- Block information
- Self object
- Previous frame pointer
- Return address
Optimizations:
- Inline caching: Cache method lookups
- Optimized send: Fast paths for common methods
- Specialized instructions: For common operations
Q23: Explain Ruby's parser and its role in metaprogramming
A:
Theoretical Role:
The parser converts Ruby code into a structure the compiler can understand. Its decisions affect how metaprogramming works.
Parser's Responsibilities:
- Token Recognition: Identify keywords, identifiers, operators
- Syntax Validation: Ensure code follows grammar rules
- AST Construction: Build node tree
- Scope Tracking: Know where variables/constants are defined
- Ambiguity Resolution: Decide if
foois a method or local variable
Parser's Impact on Metaprogramming:
1. Method vs Local Variable:
x = 1 # Parser sees assignment, treats x as variable
def foo
x # Parser sees no assignment, treats x as method call
end
2. def vs define_method:
defis parsed at compile timedefine_methodis executed at runtime- This is why
define_methodcan use variables from surrounding scope
3. Heredocs and String Interpolation:
Parser must handle nested expressions in #{}
4. Operator Overloading: Parser recognizes operators as method calls
Parser Limitations:
- Can't know runtime values
- Can't know what methods will exist
- Makes decisions based on syntax alone
Ripper - Ruby's Parser Library:
require 'ripper'
pp Ripper.sexp("def foo; end") # Shows parse tree
Q24: What is the difference between compile-time and runtime in Ruby?
A:
Theoretical Distinction:
Unlike compiled languages, Ruby's compile-time and runtime interweave because of features like eval and define_method.
Compile-Time:
When Ruby reads a file:
- Lexing: Convert text to tokens
- Parsing: Build AST from tokens
- Compilation: Generate bytecode from AST
- Scope Setup: Create lexical scope for variables/constants
At compile time, Ruby knows:
- Syntax of the code
- Lexical structure (nesting)
- Local variable assignments (to distinguish from methods)
- Method definitions (as syntax, not what they do)
Runtime:
When Ruby executes bytecode:
- Method Lookup: Find method implementations
- Variable Access: Read/write variables
- Object Creation: Allocate and initialize objects
- Control Flow: Execute conditionals, loops
At runtime, Ruby knows:
- Actual values of variables
- Which methods exist
- Object states
The Gray Area:
Some features blur the line:
eval family:
code = "def foo; 42; end"
eval(code) # Compiles AND runs at runtime
define_method:
define_method(:foo) { 42 } # Compiles block at runtime
method_missing:
- Compiled normally
- Only called at runtime when method not found
Class reopening:
class String
def new_method # Compiled at compile-time of this file
end
end
Practical Implications:
requirecompiles files at load timeautoloadcompiles when constant first accessed- Rails reloading recompiles files in development
Threading Model
Q25: Explain Ruby's threading model at the C level
A:
Theoretical Foundation:
MRI Ruby uses native threads (1:1 threading) but with a Global VM Lock (GVL).
Thread Representation:
Each Ruby thread is represented by:
- rb_thread_t structure in C
- Native thread (pthread on Unix, Windows thread on Windows)
- Ruby stack for VM execution
- C stack for C extensions
The GVL Implementation:
The GVL is implemented as a mutex with conditional variables:
- Only one thread can hold the GVL at a time
- Threads release GVL during blocking operations
- Timer thread interrupts long-running threads
Thread Scheduling:
- Thread A holds GVL, running Ruby code
- Thread B waits for GVL (sleeping)
- When Thread A blocks on I/O, it releases GVL
- Thread B acquires GVL and runs
- When I/O completes, Thread A queues for GVL
Timer Thread:
- Separate native thread
- Interrupts Ruby thread every 100ms (configurable)
- Forces thread switch if current thread runs too long
- Prevents thread starvation
Context Switching:
When switching threads:
- Save current thread's registers and stack
- Restore next thread's registers and stack
- Update current thread pointer
- Continue execution
Q26: What is the Global VM Lock (GVL) and why does it exist?
A:
Theoretical Purpose:
The GVL (also called GIL - Global Interpreter Lock) ensures that only one thread executes Ruby code at a time.
Why GVL Exists:
1. Memory Safety:
- Ruby's C internals aren't thread-safe
- Object structures would corrupt if modified concurrently
- GVL prevents race conditions in core data structures
2. GC Simplicity:
- Garbage collector assumes exclusive access
- No need for complex concurrent GC
- Simpler, more predictable behavior
3. C Extension Compatibility:
- Many C extensions aren't thread-safe
- GVL protects them automatically
- Without GVL, every extension would need careful locking
4. Implementation Simplicity:
- MRI was originally single-threaded
- Adding GVL was simpler than full thread-safety
- Keeps the implementation manageable
Trade-offs:
Disadvantages:
- CPU-bound threads can't parallelize
- One slow thread blocks others
- Doesn't use multiple cores efficiently
Advantages:
- Simpler programming model
- No locking needed for most Ruby code
- Predictable behavior
When GVL is Released:
GVL is released during:
- I/O operations (read/write/sleep)
- Blocking system calls
- C extension code that releases GVL intentionally
- Some native operations
Q27: Explain thread safety and race conditions in Ruby
A:
Theoretical Concepts:
Even with GVL, Ruby code can have race conditions because:
- GVL is released during I/O
- Multiple threads can interleave at any point
- Operations aren't atomic just because they're one line
Types of Race Conditions:
1. Check-then-act:
if @counter.zero? # Check
@counter += 1 # Act (another thread might have changed @counter)
end
2. Read-modify-write:
@counter += 1 # Not atomic!
# Actually: read, add, write - can be interrupted
3. Compound operations:
@hash[key] ||= value # Not atomic!
# Might cause multiple threads to set same key
Thread Safety Mechanisms:
1. Mutex (Mutual Exclusion):
@mutex = Mutex.new
@mutex.synchronize do
@counter += 1 # Now atomic
end
2. Monitor:
- Reentrant mutex
- Can be locked multiple times by same thread
3. ConditionVariable:
- Signal between threads
- Used with mutex for complex coordination
4. Queue (thread-safe):
queue = Queue.new
queue.push(item) # Thread-safe
item = queue.pop # Blocks if empty
5. Atomic operations (Ruby 3.0+):
require 'atomic'
counter = Atomic.new(0)
counter.update { |v| v + 1 } # Atomic increment
Q28: What are the differences between Thread, Fiber, and Ractor?
A:
Theoretical Comparison:
| Feature | Thread | Fiber | Ractor | |---------|--------|-------|--------| | Scheduling | Preemptive | Cooperative | Parallel | | Concurrency | Concurrent | Cooperative | Parallel | | Memory | Shared | Shared | Isolated | | Communication | Shared state | Same as caller | Message passing | | GVL | Subject to GVL | Subject to GVL | Separate GVL per Ractor | | Ruby Version | Always | 1.9+ | 3.0+ |
Thread (Preemptive Concurrency):
- OS or VM decides when to switch
- Can be interrupted at any time
- Shared memory requires locking
- Good for I/O-bound work
Fiber (Cooperative Concurrency):
- Explicit yielding control
- Programmer decides when to switch
- Lighter weight than threads
- Manual scheduling with
resume/yield - Great for implementing enumerators
Ractor (Parallel Execution):
- Isolated memory between Ractors
- No shared state (by default)
- Message passing for communication
- True parallelism (multiple CPU cores)
- Each Ractor has its own GVL
Memory Models:
- Thread: Everything shared, need synchronization
- Fiber: Share with parent thread, manual switching
- Ractor: Objects must be moved or copied between Ractors
Use Cases:
- Threads: Web servers, I/O operations
- Fibers: Generators, enumerators, lightweight concurrency
- Ractors: CPU-intensive parallel processing
Ruby Design Patterns
Q29: Explain common Ruby design patterns and their theoretical basis
A:
Theoretical Patterns:
Ruby's dynamic nature enables patterns that differ from static languages.
1. Singleton Pattern (Ruby style):
In Ruby, the singleton pattern is often unnecessary because:
- Modules can provide single-instance behavior
- Classes themselves are objects
# Ruby way - just use a module
module Configuration
extend self
attr_accessor :setting
end
2. Factory Pattern:
Ruby can use classes as factories:
class AnimalFactory
def self.create(type, *args)
# Classes are objects, so we can return them
Object.const_get(type.capitalize).new(*args)
end
end
3. Observer Pattern:
Ruby has built-in Observable module:
require 'observer'
class Subject
include Observable
def change
changed
notify_observers(data)
end
end
4. Strategy Pattern:
Blocks make this trivial:
def sort(array, &strategy)
strategy ||= ->(a,b) { a <=> b } # default
array.sort(&strategy)
end
5. Decorator Pattern:
Simple with modules:
class Coffee
def cost; 2; end
end
module WithMilk
def cost; super + 0.5; end
end
coffee = Coffee.new
coffee.extend(WithMilk)
Q30: Explain the concept of "Dependency Injection" in Ruby
A:
Theoretical Foundation:
Dependency Injection is about providing dependencies from outside rather than creating them inside.
Why Ruby is Different:
Ruby's dynamic typing and open classes make DI less formal:
- Can inject anything that responds to methods
- No interfaces needed
- Can mock/stub easily in tests
Types of Injection:
1. Constructor Injection:
class UserService
def initialize(repository = UserRepository.new)
@repository = repository
end
end
2. Setter Injection:
class UserService
attr_writer :repository
def repository
@repository ||= UserRepository.new
end
end
3. Method Injection:
def find_user(id, repository = UserRepository.new)
repository.find(id)
end
4. Container/Registry:
class Container
def self.services
@services ||= {}
end
def self.register(key, service)
services[key] = service
end
def self.get(key)
services[key].is_a?(Proc) ? services[key].call : services[key]
end
end
Q31: Explain metaprogramming patterns and when to use them
A:
Theoretical Classification:
1. Dynamic Method Creation:
- Use when methods follow patterns
- Avoid repetitive code
- Create methods based on data
When to use: ActiveRecord finders, DSLs When to avoid: Methods are few and unique, clarity matters
2. Method Missing:
- Use for delegation, proxies
- Handle dynamic interfaces
- Create flexible APIs
When to use: Proxy objects, dynamic responders When to avoid: Performance critical, clear interface needed
3. Class Macros:
- Use for adding behavior declaratively
- Create clean DSLs
- Encapsulate complexity
When to use: attr_accessor, validations, associations
When to avoid: When simpler code would suffice
4. Domain Specific Languages (DSLs):
- Use for configuration
- Create expressive APIs
- Hide implementation details
When to use: Configuration, routing, build tools When to avoid: When standard Ruby is clearer
5. Hook Methods:
- Use to extend frameworks
- Respond to lifecycle events
- Add cross-cutting concerns
When to use: Plugins, callbacks, logging When to avoid: When inheritance is sufficient
Metaprogramming Theory
Q32: Explain the theoretical basis of method_missing
A:
Theoretical Foundation:
method_missing is a hook in the method lookup process, not a performance optimization or a way to avoid writing methods.
The Lookup Process with method_missing:
- Ruby searches for method (as per lookup path)
- If not found, it restarts search looking for
method_missing - If found, calls it with the original method name and arguments
- If not found, raises NoMethodError
The Contract:
Proper method_missing usage requires:
- Override
respond_to_missing?consistently - Call
superfor unhandled cases - Maintain the expected arity pattern
- Document the dynamic behavior
Theoretical Implications:
- Transparency: Objects should appear to have the methods
- Performance: Each call goes through failed lookup first
- Debugging: Can hide method definitions
- Introspection: Must maintain
methodsandrespond_to?
Q33: Explain define_method vs def at the C level
A:
Theoretical Distinction:
def (compile-time):
- Parsed when file is read
- Method added to class at compile time
- Method name known to parser
- Stored in method table immediately
define_method (runtime):
- Executed when code runs
- Method added at that moment
- Method name can be dynamic
- Uses block as method body
C-Level Implementation:
When you use def:
- Parser creates a
NODE_DEFNin AST - Compiler generates
putiseqinstruction - VM adds method to class at load time
When you use define_method:
- Block is compiled (maybe at load time)
define_methodcall creates a method object- Method object is added to class at runtime
- Block's binding is captured
Method Objects:
Both create rb_method_definition_t structures:
defcreates one at compile timedefine_methodcreates one at runtime- They're identical once created
Q34: What is the theoretical basis of eval and its variants?
A:
Theoretical Overview:
eval family functions inject new code into the running program, causing runtime compilation.
The Eval Family:
-
eval(string):- Compiles and executes in current context
- Can access local variables
- Performance overhead (compilation at runtime)
-
instance_eval(string):- Changes
selfto receiver - Can access receiver's instance variables
- Cannot access original local variables
- Changes
-
class_eval(string):- Changes
selfto class - Defines instance methods
- Behaves like reopening class
- Changes
-
binding.eval(string):- Uses captured binding as context
- Can access variables from that binding
- Powerful but potentially dangerous
Security Implications:
- Code Injection: If string comes from user input
- Sandbox Escape: Can access sensitive data
- Performance: Compiles each time
- Debugging: Hard to trace errors
Alternatives:
public_sendfor dynamic callsdefine_methodfor dynamic definitions- Blocks and procs for behavior passing
Q35: Explain binding objects and their theoretical role
A:
Theoretical Definition:
A Binding object captures the execution context at a point in time, including:
- Local variables
- Self object
- Current class/module
- Stack trace information
- Block context
Binding Internals:
In C, a rb_binding_t contains:
env: Environment (local variables)path: File pathlineno: Line numberself: Current objectscope: Class/module scope
Uses of Binding:
-
Debugging:
- Capture context at breakpoints
- Inspect variables later
-
Templating (ERB):
- Evaluate templates in binding context
- Access variables from caller
-
REPLs (IRB/Pry):
- Capture main binding
- Evaluate user input in that context
-
DSLs:
- Pass binding to evaluation methods
- Control what's accessible
Theoretical Implications:
- Bindings prevent GC of captured variables
- Each binding has memory overhead
- Can break encapsulation if misused
- Essential for advanced metaprogramming
Ruby Internals
Q36: Explain the structure of a Ruby object in C
A:
Theoretical Layout:
In MRI, every Ruby object is represented by an RVALUE structure, which is a union of all possible object types.
Basic Structure (simplified):
struct RBasic {
VALUE flags; /* object status flags (GC, frozen, taint, etc.) */
VALUE klass; /* class of the object */
};
struct RObject {
struct RBasic basic;
struct st_table *ivptr; /* instance variable table */
// ... more fields
};
struct RClass {
struct RBasic basic;
struct st_table *iv_tbl; /* class instance variables */
struct st_table *m_tbl; /* method table */
VALUE super; /* superclass */
// ... more fields
};
The VALUE Type:
VALUE is a typedef for unsigned long that can hold:
- Immediate values: Fixnums, Symbols, true, false, nil
- Pointers: All other objects
- Flonums: Float immediates (on some platforms)
Immediate Values:
- Fixnum: Lowest bit = 1 (shifted left)
- Symbol: Special handling
- Special constants: Qtrue, Qfalse, Qnil
Object Flags:
The flags field contains bit flags for:
- GC marking
- Object type
- Frozen status
- Taint status (old Ruby versions)
- Trusted/untrusted
- etc.
Method Tables:
Classes store methods in m_tbl (hash table):
- Key: method name (symbol ID)
- Value: method definition structure
Q37: How does Ruby implement method lookup in C?
A:
Theoretical Process:
The method lookup is implemented in vm_method.c and called from the VM.
Simplified C Flow:
// Pseudocode of method lookup
VALUE rb_method_lookup(VALUE klass, ID mid) {
// 1. Check the class
while (klass) {
// 2. Look in method table
rb_method_entry_t *me = search_method_table(klass, mid);
if (me) return me;
// 3. Move to superclass
klass = RCLASS_SUPER(klass);
}
// 4. Not found - trigger method_missing
return rb_method_missing(...);
}
Inline Caching:
To speed up repeated calls:
- First call does full lookup
- Result cached at call site
- Next call checks if class matches
- If match, use cached method
- If not, do full lookup again
Cache Structure:
struct icache {
VALUE klass; // Class that had the method
ID mid; // Method ID
rb_method_entry_t *me; // Cached method
};
Method Entry Structure:
Each method entry contains:
- Def type: ISEQ (Ruby), C method, etc.
- Body: Pointer to implementation
- Visibility: Public, private, protected
- Alias count: For tracking
Q38: Explain Ruby's VM registers and stack operations
A:
Theoretical VM Architecture:
YARV is a stack-based VM with several key registers.
VM Registers:
- PC (Program Counter): Points to current instruction
- SP (Stack Pointer): Top of evaluation stack
- CFP (Control Frame Pointer): Current stack frame
- EP (Environment Pointer): Local variable scope
Stack Structure:
Each stack frame contains:
- Previous frame: Link to caller
- Self: Current object
- Method ID: Current method
- PC: Return address
- SP: Stack pointer at entry
- EP: Environment for locals
- Block pointer: If block given
Instruction Format:
Instructions are 16-bit opcodes with optional operands:
[opcode] [operand1] [operand2] ...
Common Instructions:
- putobject: Push object onto stack
- send: Call method
- branch: Conditional jump
- leave: Return from method
- getlocal: Get local variable
- setlocal: Set local variable
Example Execution:
For a + b:
getlocal a # Push a onto stack
getlocal b # Push b onto stack
send :+, 1 # Call + with 1 argument (b)
# Result left on stack
Q39: How does Ruby handle exceptions at the C level?
A:
Theoretical Exception Mechanism:
Ruby exceptions use C's setjmp/longjmp for non-local jumps.
Exception Handling Flow:
setjmpsaves current context (registers, stack)- Code executes normally
- If error occurs,
longjmprestores saved context - Control transfers to rescue block
In Ruby VM:
Each begin/rescue creates a jump buffer:
struct rb_vm_tag {
VALUE tag; /* exception object */
struct rb_vm_tag *prev; /* previous tag */
jmp_buf buf; /* saved context */
int state; /* exception state */
};
Exception Table:
Methods have an exception table mapping:
- Instruction ranges to rescue blocks
- Instruction ranges to ensure blocks
- Which exception classes to catch
Rescue Process:
- Exception raised with
rb_raise - VM searches for matching rescue
- If found,
longjmpto that handler - Handler runs, stack unwound
- Ensure blocks executed along the way
Stack Unwinding:
When exception propagates:
- Run ensure blocks for each frame
- Release resources
- Move to next frame
- Repeat until rescue found
Q40: Explain Ruby's internal representation of strings and symbols
A:
Theoretical String Representation:
Ruby strings are represented by RString structure:
struct RString {
struct RBasic basic;
long len; /* length in bytes */
union {
char *ptr; /* heap pointer for long strings */
char ary[24]; /* embedded for short strings */
} as;
union {
long capa; /* capacity (heap strings) */
struct { /* shared strings */
VALUE shared;
long offset;
} shared;
} aux;
};
String Optimizations:
- Embedded Strings: <=24 bytes stored in object itself
- Shared Strings: Copy-on-write for substrings
- Deduped Strings: Same content can share storage
- Coderange: ASCII-only, binary, etc. flags
Symbol Representation:
Symbols are unique identifiers represented by IDs:
struct RSymbol {
struct RBasic basic;
ID id; /* symbol ID */
VALUE fstr; /* frozen string (cached) */
};
Symbol Table:
All symbols are stored in a global symbol table:
- Each symbol has a unique ID
- ID to string mapping
- String to ID mapping
- Symbols are never garbage collected (before Ruby 2.2)
Symbol GC (Ruby 2.2+):
- Dynamic symbols (created at runtime) can be GC'd
- Immortal symbols (parsed from source) persist
- Uses "mortal" concept for temporary symbols
Encoding Awareness:
Both strings and symbols track encoding:
- Each string has encoding information
- Operations respect encoding
- Transcoding when needed