Ruby

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, calls to_s on objects
  • print - prints without a newline, calls to_s on objects
  • p - prints with a newline, calls inspect (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:

  • map returns [false, true, false]
  • select returns [2]

Q30: What's the difference?

def method1
  {key: "value"}
end

def method2
  {key: "value"} if true
end

A:

  • method1 always returns a hash
  • method2 returns a hash when condition is true, but returns nil if 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:

  1. Objects and Classes: Every object is an instance of a class. Classes themselves are objects (instances of Class).

  2. The Class Hierarchy: All classes ultimately inherit from BasicObject → Object → Kernel (module) → YourClass.

  3. Metaclasses (Eigenclasses): Every object has a hidden class called its metaclass (or eigenclass, or singleton class) that stores object-specific methods.

  4. 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:

  1. 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
  2. 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
  3. Method Storage:

    • Methods are stored in the class, not in individual objects
    • Objects contain a pointer to their class (the klass pointer)
    • This is why 1000 objects of the same class use much less memory than 1000 objects with singleton methods
  4. The klass Pointer:

    • Every object has an internal klass pointer that references its class
    • When you call a method, Ruby follows this pointer to find the method
  5. Singleton Classes:

    • When you define a method on a single object, Ruby creates a singleton class
    • The object's klass pointer 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).

  1. class (keyword):

    • A language construct for defining classes
    • Creates a new scope and sets self to the class being defined
    • Example: class Person; end
  2. Class (class object):

    • The actual class object that represents all classes
    • Class is an instance of Class (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:

  • BasicObject is the root of the class hierarchy
  • Class inherits from Module, which inherits from Object, which inherits from BasicObject
  • But BasicObject.class returns Class
  • This circular reference is hardcoded in the interpreter

The Class-Module Relationship:

  • Class is a subclass of Module
  • 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:

  1. If the class doesn't exist, it creates a new class object
  2. If the class already exists, it reopens the existing class
  3. Any methods defined inside are added to the existing class

Implications:

  1. Monkey Patching: You can modify built-in classes
  2. Refinements: A way to scope monkey patches (introduced in Ruby 2.0)
  3. Method Racing: Later definitions override earlier ones
  4. Load Order Matters: The last loaded definition wins

Theoretical Problems:

  1. Global Impact: Changes affect all code, including gems
  2. Naming Conflicts: Different libraries might patch the same method differently
  3. Debugging Difficulty: It's not obvious where a method came from
  4. 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:

  1. Ruby creates an anonymous class (inclusion class) that wraps the module
  2. This anonymous class is inserted into the ancestor chain right above the including class
  3. The anonymous class's superclass is set to the including class's superclass

When you prepend a module:

  1. Similar anonymous class is created
  2. But it's inserted below the including class (so it gets looked at first)
  3. 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:

  1. Last included wins (for method definition)
  2. 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:

  1. Ruby looks at the current class/module's position in the ancestor chain
  2. It finds the next entity in the chain (could be another module or a class)
  3. It looks for the same method name in that entity
  4. If found, it calls it; if not, it continues up the chain

Important Rules:

  • super doesn'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: nil if accessed before assignment (no error)
  • Storage: Stored in the object's RBasic structure 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:

  1. All local variables in scope
  2. The current value of self
  3. The current class/module context
  4. 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):

  1. When a block is created, Ruby allocates a rb_binding_t structure
  2. This structure holds pointers to the current execution context
  3. It increments reference counts on captured variables
  4. 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, self in 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: self changes 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:

  1. Lexical Scope (current nesting): Starting from the innermost nesting and moving outward
  2. Inheritance Hierarchy: If not found lexically, look up the superclass chain
  3. Top Level: Finally, look at Object (and therefore Kernel and BasicObject)

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 level
  • A::VALUE - start in module A
  • self.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_missing can 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):

  • self is main (an instance of Object)
  • 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):

  • self is the class object itself
  • Example: class Person; self end returns Person
  • This is why class methods are defined as def self.method

3. Inside an Instance Method:

  • self is 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:

  • self is 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: self is the module
  • In module instance methods: self will be the including object

6. Inside a Block:

  • Blocks inherit the self from where they're defined
  • This is why blocks are closures - they capture self
  • Procs also capture self from creation context

7. Inside a Lambda:

  • Same as blocks - captures self from creation context
  • Unlike blocks, lambdas check arity

8. Inside a Singleton Method (def obj.method):

  • self is the object obj
  • This is how singleton methods know which object they're on

The self Switch: Ruby provides ways to change self:

  • instance_eval: changes self to the receiver
  • class_eval: changes self to 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 self to 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 self to 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:

  • self becomes 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_eval when you want to:

    • Access an object's internal state
    • Define singleton methods
    • Create DSLs (like configure blocks)
  • Use class_eval when 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:

  1. At the top level: There is no current class; methods become private methods of Object

  2. Inside a class definition: The current class is that class

  3. Inside a method: The current class is the class of the object (but def inside a method creates a method on the containing class? Actually, this is tricky)

  4. Inside class_eval: The current class becomes the receiver

  5. Inside instance_eval: The current class becomes the singleton class of the receiver

The class Keyword Effect:

  • class Person; end sets the current class to Person
  • After end, it reverts to the previous current class

The def self.method Mystery: When you write def self.method:

  1. Ruby evaluates self (which might be a class or object)
  2. It gets that object's singleton class
  3. It makes that singleton class the current class for the method definition
  4. 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:

  1. Managed Heap for Ruby objects
  2. System Stack for method calls and local variables
  3. 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:

  1. Ruby finds a free slot in an existing heap page
  2. If no free slots, allocate a new page
  3. If no more pages, trigger garbage collection
  4. 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 RBasic plus 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:

  1. Minor GC: Young generation collection

    • Fast, frequent
    • Promotes surviving objects to old generation
  2. 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 objects
  • derailed_benchmarks: For Rails memory analysis
  • heapy: 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:

  1. Move objects to consolidate free space
  2. Update all references to moved objects
  3. Requires write barriers and careful coordination
  4. 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:

  1. GC marks are stored in a separate bitmap (not in object pages)
  2. Object pages remain read-only during GC
  3. Forked processes can share pages longer
  4. 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.c in 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 stack
  • send :puts, 1 # Call method with 1 argument
  • leave # 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:

  1. Token Recognition: Identify keywords, identifiers, operators
  2. Syntax Validation: Ensure code follows grammar rules
  3. AST Construction: Build node tree
  4. Scope Tracking: Know where variables/constants are defined
  5. Ambiguity Resolution: Decide if foo is 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:

  • def is parsed at compile time
  • define_method is executed at runtime
  • This is why define_method can 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:

  1. Lexing: Convert text to tokens
  2. Parsing: Build AST from tokens
  3. Compilation: Generate bytecode from AST
  4. 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:

  1. Method Lookup: Find method implementations
  2. Variable Access: Read/write variables
  3. Object Creation: Allocate and initialize objects
  4. 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:

  • require compiles files at load time
  • autoload compiles 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:

  1. Thread A holds GVL, running Ruby code
  2. Thread B waits for GVL (sleeping)
  3. When Thread A blocks on I/O, it releases GVL
  4. Thread B acquires GVL and runs
  5. 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:

  1. Save current thread's registers and stack
  2. Restore next thread's registers and stack
  3. Update current thread pointer
  4. 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:

  1. GVL is released during I/O
  2. Multiple threads can interleave at any point
  3. 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:

  1. Ruby searches for method (as per lookup path)
  2. If not found, it restarts search looking for method_missing
  3. If found, calls it with the original method name and arguments
  4. If not found, raises NoMethodError

The Contract:

Proper method_missing usage requires:

  1. Override respond_to_missing? consistently
  2. Call super for unhandled cases
  3. Maintain the expected arity pattern
  4. 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 methods and respond_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:

  1. Parser creates a NODE_DEFN in AST
  2. Compiler generates putiseq instruction
  3. VM adds method to class at load time

When you use define_method:

  1. Block is compiled (maybe at load time)
  2. define_method call creates a method object
  3. Method object is added to class at runtime
  4. Block's binding is captured

Method Objects: Both create rb_method_definition_t structures:

  • def creates one at compile time
  • define_method creates 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:

  1. eval(string):

    • Compiles and executes in current context
    • Can access local variables
    • Performance overhead (compilation at runtime)
  2. instance_eval(string):

    • Changes self to receiver
    • Can access receiver's instance variables
    • Cannot access original local variables
  3. class_eval(string):

    • Changes self to class
    • Defines instance methods
    • Behaves like reopening class
  4. 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_send for dynamic calls
  • define_method for 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 path
  • lineno: Line number
  • self: Current object
  • scope: Class/module scope

Uses of Binding:

  1. Debugging:

    • Capture context at breakpoints
    • Inspect variables later
  2. Templating (ERB):

    • Evaluate templates in binding context
    • Access variables from caller
  3. REPLs (IRB/Pry):

    • Capture main binding
    • Evaluate user input in that context
  4. 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:

  1. First call does full lookup
  2. Result cached at call site
  3. Next call checks if class matches
  4. If match, use cached method
  5. 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:

  1. PC (Program Counter): Points to current instruction
  2. SP (Stack Pointer): Top of evaluation stack
  3. CFP (Control Frame Pointer): Current stack frame
  4. 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:

  1. setjmp saves current context (registers, stack)
  2. Code executes normally
  3. If error occurs, longjmp restores saved context
  4. 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:

  1. Exception raised with rb_raise
  2. VM searches for matching rescue
  3. If found, longjmp to that handler
  4. Handler runs, stack unwound
  5. Ensure blocks executed along the way

Stack Unwinding:

When exception propagates:

  1. Run ensure blocks for each frame
  2. Release resources
  3. Move to next frame
  4. 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:

  1. Embedded Strings: <=24 bytes stored in object itself
  2. Shared Strings: Copy-on-write for substrings
  3. Deduped Strings: Same content can share storage
  4. 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