Ruby Logo

Frozen String Literals & Immutability

Optimize memory and improve performance with frozen string literals and immutable string patterns.

Home Ruby Frozen String Literals & Immutability

Frozen String Literals

Supercharge your Ruby applications with # frozen_string_literal: true - unlocking memory efficiency, performance gains, and bulletproof immutability

🎯 Why Frozen String Literals Matter

Without Frozen Strings

# Every string literal creates new object
1000.times do
  puts "Processing request"  # 1000 objects!
end

# Memory usage grows unnecessarily
# GC pressure increases
# Performance degrades

With Frozen Strings

# frozen_string_literal: true
1000.times do
  puts "Processing request"  # 1 object!
end

# Massive memory savings
# Reduced GC pressure
# Better performance

🚀 Core Benefits

🧠

Memory Efficiency

Identical strings share the same memory location, reducing allocation overhead by up to 80%

Performance Boost

No defensive copying needed, faster hash lookups, reduced garbage collection pressure

🔒

Thread Safety

Immutable strings are inherently safe to share between threads without locks

🛡️

Bug Prevention

Prevents accidental string modification that can lead to hard-to-debug issues

💼 Real-World Use Cases

Configuration Files

Database URLs, API endpoints, and app constants benefit from memory sharing

Email Templates

Reusable email subjects and HTML templates get massive memory savings

Web Applications

HTTP headers, status messages, and route patterns used repeatedly

💡 Quick Start

# Add this magic comment as the first line
# frozen_string_literal: true

# Now all string literals are automatically frozen!
app_name = "My Ruby App"  # frozen: true
version = "1.0.0"        # frozen: true

# Create mutable copy when needed
mutable_name = +app_name     # frozen: false
mutable_name << " v2"       # ✅ Works!

That's it! Just one line at the top of your Ruby file enables massive performance benefits automatically.

🔧 Practical Examples

⚙️ Configuration & Constants

# frozen_string_literal: true

class WebApp
  # All these constants share memory!
  APP_NAME = "MyRubyApp"
  VERSION = "2.1.0"
  API_BASE = "https://api.example.com"

  # HTTP constants - perfect for frozen strings
  HTTP_OK = "200 OK"
  NOT_FOUND = "404 Not Found"
  SERVER_ERROR = "500 Internal Server Error"
end

# Memory usage: 1 object per unique string!

🛠️ Working with Mutable Strings

# frozen_string_literal: true

def build_message(name, action)
  # ❌ This would fail - trying to modify frozen string
  # message = "Hello"
  # message << " #{name}"  # FrozenError!

  # ✅ Use string interpolation instead
  message = "Hello #{name}, you just #{action}!"

  # ✅ Or create mutable copy when needed
  mutable = +"Processing: "  # Unary + makes it mutable
  mutable << name
  mutable << " - " << action

  [message, mutable]
end


# String interpolation creates new objects (not frozen)
greeting = "Welcome #{user.name}!"  # frozen: false
url = "#{API_BASE}/users/#{user.id}"     # frozen: false

Performance Impact and Optimization

Memory Usage Comparison

# frozen_string_literal: true

class PerformanceAnalysis
  def self.memory_usage_demo
    puts "=== Memory Usage Analysis ==="

    # Simulate a web application with many constant strings
    def self.without_frozen_strings
      # Simulating code without frozen string literals
      constants = []
      1000.times do
        constants << "application/json".dup
        constants << "text/html".dup
        constants << "image/png".dup
        constants << "GET".dup
        constants << "POST".dup
      end
      constants
    end

    def self.with_frozen_strings
      # With frozen string literals
      constants = []
      1000.times do
        constants << "application/json"
        constants << "text/html"
        constants << "image/png"
        constants << "GET"
        constants << "POST"
      end
      constants
    end

    # Analyze object creation
    before_frozen = ObjectSpace.count_objects[:T_STRING]
    frozen_constants = with_frozen_strings
    after_frozen = ObjectSpace.count_objects[:T_STRING]

    puts "Frozen strings - Objects created: #{after_frozen - before_frozen}"

    before_mutable = ObjectSpace.count_objects[:T_STRING]
    mutable_constants = without_frozen_strings
    after_mutable = ObjectSpace.count_objects[:T_STRING]

    puts "Mutable strings - Objects created: #{after_mutable - before_mutable}"

    # Check unique object IDs
    frozen_unique = frozen_constants.map(&:object_id).uniq.length
    mutable_unique = mutable_constants.map(&:object_id).uniq.length

    puts "Frozen - Unique objects: #{frozen_unique}"
    puts "Mutable - Unique objects: #{mutable_unique}"
    puts "Memory efficiency: #{((mutable_unique - frozen_unique).to_f / mutable_unique * 100).round(2)}% reduction"
  end

  def self.string_creation_benchmark
    require 'benchmark'

    puts "\n=== String Creation Benchmark ==="

    n = 100_000

    Benchmark.bm(25) do |x|
      x.report("Frozen literals") do
        n.times do
          str = "Hello World"
          str.length
        end
      end

      x.report("Mutable copies") do
        n.times do
          str = "Hello World".dup
          str.length
        end
      end

      x.report("String interpolation") do
        n.times do
          str = "#{'Hello'} #{'World'}"
          str.length
        end
      end

      x.report("String concatenation") do
        n.times do
          str = "Hello" + " " + "World"
          str.length
        end
      end
    end
  end

  def self.hash_key_performance
    puts "\n=== Hash Key Performance ==="

    # Frozen strings make excellent hash keys
    hash_with_frozen = {}
    hash_with_mutable = {}

    # Pre-populate with frozen keys
    1000.times do |i|
      hash_with_frozen["key_#{i}"] = "value_#{i}"
    end

    # Pre-populate with mutable keys
    1000.times do |i|
      hash_with_mutable["key_#{i}".dup] = "value_#{i}".dup
    end

    require 'benchmark'

    n = 10_000

    Benchmark.bm(20) do |x|
      x.report("Frozen key lookup") do
        n.times do
          hash_with_frozen["key_500"]
        end
      end

      x.report("Mutable key lookup") do
        n.times do
          hash_with_mutable["key_500".dup]
        end
      end
    end
  end

  def self.gc_impact_analysis
    puts "\n=== Garbage Collection Impact ==="

    require 'benchmark'

    def self.create_many_strings(frozen: true)
      if frozen
        1000.times.map { "This is a test string for GC analysis" }
      else
        1000.times.map { "This is a test string for GC analysis".dup }
      end
    end

    GC.disable

    # Measure GC with frozen strings
    gc_before = GC.stat[:total_allocated_objects]
    frozen_strings = create_many_strings(frozen: true)
    gc_after_frozen = GC.stat[:total_allocated_objects]

    # Measure GC with mutable strings
    mutable_strings = create_many_strings(frozen: false)
    gc_after_mutable = GC.stat[:total_allocated_objects]

    GC.enable

    puts "Objects allocated (frozen): #{gc_after_frozen - gc_before}"
    puts "Objects allocated (mutable): #{gc_after_mutable - gc_after_frozen}"
    puts "Difference: #{(gc_after_mutable - gc_after_frozen) - (gc_after_frozen - gc_before)}"
  end
end

PerformanceAnalysis.memory_usage_demo
# PerformanceAnalysis.string_creation_benchmark  # Uncomment to run benchmark
# PerformanceAnalysis.hash_key_performance       # Uncomment to run benchmark
PerformanceAnalysis.gc_impact_analysis

Real-World Applications

Configuration and Constants

# frozen_string_literal: true

class ConfigurationExample
  # Application constants - all frozen by default
  APP_NAME = "MyWebApp"
  VERSION = "1.2.3"
  DEFAULT_LOCALE = "en"

  # HTTP status messages
  HTTP_MESSAGES = {
    200 => "OK",
    201 => "Created",
    400 => "Bad Request",
    401 => "Unauthorized",
    404 => "Not Found",
    500 => "Internal Server Error"
  }.freeze

  # MIME types
  MIME_TYPES = {
    ".html" => "text/html",
    ".css" => "text/css",
    ".js" => "application/javascript",
    ".json" => "application/json",
    ".png" => "image/png",
    ".jpg" => "image/jpeg"
  }.freeze

  # Database configuration
  class DatabaseConfig
    HOST = "localhost"
    PORT = "5432"
    DATABASE = "myapp_production"

    def self.connection_string
      # String interpolation creates new string
      "postgresql://#{HOST}:#{PORT}/#{DATABASE}"
    end

    def self.connection_params
      {
        host: HOST,
        port: PORT,
        database: DATABASE
      }
    end
  end

  def self.demonstrate_configuration
    puts "=== Configuration Usage ==="

    puts "App: #{APP_NAME} v#{VERSION}"
    puts "Locale: #{DEFAULT_LOCALE}"
    puts "Connection: #{DatabaseConfig.connection_string}"

    # All string literals are frozen
    constants = [APP_NAME, VERSION, DEFAULT_LOCALE]
    constants.each do |const|
      puts "#{const} frozen? #{const.frozen?}"
    end

    # Hash values are also frozen
    HTTP_MESSAGES.each do |code, message|
      puts "#{code}: #{message} (frozen: #{message.frozen?})"
    end
  end

  # Logger with frozen format strings
  class Logger
    LOG_LEVELS = {
      debug: "DEBUG",
      info: "INFO",
      warn: "WARN",
      error: "ERROR"
    }.freeze

    def self.log(level, message)
      timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
      level_str = LOG_LEVELS[level] || "UNKNOWN"

      # Format string is frozen, but result is mutable
      formatted = "[#{timestamp}] #{level_str}: #{message}"
      puts formatted

      formatted
    end

    def self.demonstrate_logging
      puts "\n=== Logging Example ==="

      messages = [
        [:info, "Application started"],
        [:debug, "Processing request"],
        [:warn, "High memory usage detected"],
        [:error, "Database connection failed"]
      ]

      messages.each do |level, msg|
        result = log(level, msg)
        puts "  Log result frozen? #{result.frozen?}"
      end
    end
  end

  def self.demonstrate_template_system
    puts "\n=== Template System ==="

    # Email templates with frozen literals
    class EmailTemplates
      WELCOME_SUBJECT = "Welcome to %s!"
      WELCOME_BODY = <<~EMAIL
        Dear %s,

        Welcome to %s! We're excited to have you on board.

        Best regards,
        The %s Team
      EMAIL

      RESET_SUBJECT = "Password Reset Request"
      RESET_BODY = <<~EMAIL
        Hello %s,

        You requested a password reset. Click the link below:
        %s

        If you didn't request this, please ignore this email.
      EMAIL

      def self.welcome_email(name, app_name)
        {
          subject: WELCOME_SUBJECT % app_name,
          body: WELCOME_BODY % [name, app_name, app_name]
        }
      end

      def self.reset_email(name, reset_link)
        {
          subject: RESET_SUBJECT,
          body: RESET_BODY % [name, reset_link]
        }
      end
    end

    # Demonstrate template usage
    welcome = EmailTemplates.welcome_email("Alice", APP_NAME)
    reset = EmailTemplates.reset_email("Bob", "https://example.com/reset/123")

    puts "Welcome Subject: #{welcome[:subject]}"
    puts "Welcome Subject frozen? #{welcome[:subject].frozen?}"
    puts "\nReset Subject: #{reset[:subject]}"
    puts "Reset Subject frozen? #{reset[:subject].frozen?}"
  end
end

ConfigurationExample.demonstrate_configuration
ConfigurationExample::Logger.demonstrate_logging
ConfigurationExample.demonstrate_template_system

API and Web Development

# frozen_string_literal: true

class WebDevelopmentExample
  # HTTP-related constants
  module HTTPConstants
    METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS].freeze

    HEADERS = {
      content_type: "Content-Type",
      authorization: "Authorization",
      user_agent: "User-Agent",
      accept: "Accept",
      cache_control: "Cache-Control"
    }.freeze

    STATUS_CODES = {
      ok: 200,
      created: 201,
      no_content: 204,
      bad_request: 400,
      unauthorized: 401,
      forbidden: 403,
      not_found: 404,
      unprocessable_entity: 422,
      internal_server_error: 500
    }.freeze
  end

  # API response builder
  class APIResponse
    def self.success(data, message = "Success")
      build_response(
        status: "success",
        data: data,
        message: message
      )
    end

    def self.error(message, code = "error")
      build_response(
        status: "error",
        error: {
          code: code,
          message: message
        }
      )
    end

    def self.paginated(data, page, per_page, total)
      build_response(
        status: "success",
        data: data,
        pagination: {
          page: page,
          per_page: per_page,
          total: total,
          pages: (total.to_f / per_page).ceil
        }
      )
    end

    private

    def self.build_response(response_data)
      # All string keys are frozen, but the hash structure is new
      {
        timestamp: Time.now.iso8601,
        **response_data
      }
    end
  end

  # Route handling
  class Router
    ROUTES = {
      "GET /users" => :list_users,
      "POST /users" => :create_user,
      "GET /users/:id" => :show_user,
      "PUT /users/:id" => :update_user,
      "DELETE /users/:id" => :delete_user
    }.freeze

    def self.match_route(method, path)
      route_key = "#{method} #{path}"

      # Direct match first
      return ROUTES[route_key] if ROUTES.key?(route_key)

      # Pattern matching for parameterized routes
      ROUTES.each do |pattern, action|
        if pattern.include?(":") && match_pattern?(pattern, route_key)
          return action
        end
      end

      nil
    end

    def self.match_pattern?(pattern, route)
      pattern_parts = pattern.split(" ", 2)[1].split("/")
      route_parts = route.split(" ", 2)[1].split("/")

      return false if pattern_parts.length != route_parts.length

      pattern_parts.zip(route_parts).all? do |pattern_part, route_part|
        pattern_part.start_with?(":") || pattern_part == route_part
      end
    end

    def self.demonstrate_routing
      puts "=== Route Matching ==="

      test_routes = [
        ["GET", "/users"],
        ["POST", "/users"],
        ["GET", "/users/123"],
        ["PUT", "/users/456"],
        ["DELETE", "/users/789"],
        ["GET", "/unknown"]
      ]

      test_routes.each do |method, path|
        action = match_route(method, path)
        puts "#{method} #{path} -> #{action || 'Not Found'}"
      end
    end
  end

  # JSON API serializer
  class JSONSerializer
    MIME_TYPE = "application/json"
    CHARSET = "utf-8"

    def self.serialize(data, options = {})
      # Content-Type header is frozen
      headers = {
        HTTPConstants::HEADERS[:content_type] => "#{MIME_TYPE}; charset=#{CHARSET}"
      }

      serialized = case data
                   when Hash
                     serialize_hash(data)
                   when Array
                     serialize_array(data)
                   else
                     { value: data }
                   end

      {
        headers: headers,
        body: JSON.generate(serialized)
      }
    end

    def self.serialize_hash(hash)
      # Transform keys to camelCase if requested
      hash.transform_keys { |key| key.to_s.frozen? ? key : key.to_s }
    end

    def self.serialize_array(array)
      {
        items: array,
        count: array.length
      }
    end

    def self.demonstrate_serialization
      puts "\n=== JSON Serialization ==="

      test_data = [
        { name: "Alice", age: 30, email: "alice@example.com" },
        [1, 2, 3, 4, 5],
        "Simple string value"
      ]

      test_data.each do |data|
        result = serialize(data)
        puts "Data: #{data.inspect}"
        puts "Headers: #{result[:headers]}"
        puts "Body: #{result[:body]}"
        puts "Body frozen? #{result[:body].frozen?}"
        puts
      end
    end
  end

  def self.demonstrate_api_usage
    puts "=== API Usage Example ==="

    # Simulate API responses
    user_data = { id: 1, name: "Alice", email: "alice@example.com" }
    users_list = [user_data, { id: 2, name: "Bob", email: "bob@example.com" }]

    responses = [
      APIResponse.success(user_data, "User retrieved"),
      APIResponse.error("User not found", "not_found"),
      APIResponse.paginated(users_list, 1, 10, 25)
    ]

    responses.each_with_index do |response, i|
      puts "Response #{i + 1}:"
      puts JSON.pretty_generate(response)
      puts
    end
  end
end

WebDevelopmentExample::Router.demonstrate_routing
WebDevelopmentExample::JSONSerializer.demonstrate_serialization
WebDevelopmentExample.demonstrate_api_usage

Migration and Best Practices

Migrating to Frozen String Literals

# frozen_string_literal: true

class MigrationGuide
  def self.common_migration_issues
    puts "=== Common Migration Issues ==="

    # Issue 1: String modification patterns
    def self.old_pattern_demo
      # This would fail with frozen strings
      begin
        message = "Hello"
        message << " World"  # FrozenError
        message
      rescue FrozenError => e
        puts "Error: #{e.message}"

        # Fixed version
        message = "Hello"
        message = message + " World"  # Creates new string
        puts "Fixed: #{message}"
      end
    end

    old_pattern_demo

    # Issue 2: String buffer patterns
    def self.string_buffer_demo
      puts "\nString Buffer Pattern:"

      # Old (problematic) pattern
      def self.old_build_string(items)
        result = ""  # This becomes frozen
        items.each { |item| result << item }  # Would fail
        result
      rescue FrozenError
        # New pattern
        result = +""  # Explicitly mutable
        items.each { |item| result << item }
        result
      end

      # Better pattern
      def self.new_build_string(items)
        items.join  # More efficient anyway
      end

      items = ["a", "b", "c", "d"]
      puts "Old pattern fixed: #{old_build_string(items)}"
      puts "New pattern: #{new_build_string(items)}"
    end

    string_buffer_demo

    # Issue 3: Default parameter modification
    def self.default_parameter_demo
      puts "\nDefault Parameter Pattern:"

      # Problematic pattern
      def problematic_method(options = {})
        options[:default] = "value"  # Modifies frozen hash
        options
      rescue FrozenError => e
        puts "Error with options: #{e.message}"
        nil
      end

      # Fixed pattern
      def fixed_method(options = {})
        options = options.dup  # Create mutable copy
        options[:default] = "value"
        options
      end

      result = problematic_method
      puts "Problematic result: #{result}"

      result = fixed_method
      puts "Fixed result: #{result}"
    end

    default_parameter_demo
  end

  def self.migration_tools
    puts "\n=== Migration Tools and Techniques ==="

    # Tool 1: Unary plus operator
    def self.unary_plus_demo
      frozen_string = "I am frozen"
      mutable_string = +frozen_string  # Unary plus creates mutable copy

      puts "Original frozen? #{frozen_string.frozen?}"
      puts "Mutable copy frozen? #{mutable_string.frozen?}"

      mutable_string << " but now I'm not!"
      puts "Modified: #{mutable_string}"
    end

    unary_plus_demo

    # Tool 2: -@ operator for deduplication
    def self.deduplication_demo
      puts "\nString Deduplication:"

      # Create multiple instances of the same string
      strings = 5.times.map { "duplicate".dup }
      puts "Before deduplication: #{strings.map(&:object_id).uniq.length} unique objects"

      # Deduplicate using -@ operator
      deduplicated = strings.map { |s| -s }
      puts "After deduplication: #{deduplicated.map(&:object_id).uniq.length} unique objects"
      puts "All point to same object? #{deduplicated.map(&:object_id).uniq.length == 1}"
    end

    deduplication_demo

    # Tool 3: String modification helpers
    module StringHelpers
      def self.safe_append(string, suffix)
        if string.frozen?
          string + suffix
        else
          string << suffix
        end
      end

      def self.safe_modify(string, &block)
        working_copy = string.frozen? ? string.dup : string
        block.call(working_copy)
        working_copy
      end
    end

    def self.helper_demo
      puts "\nString Helper Methods:"

      frozen_str = "Hello"
      mutable_str = "Hello".dup

      result1 = StringHelpers.safe_append(frozen_str, " World")
      result2 = StringHelpers.safe_append(mutable_str, " World")

      puts "Frozen result: #{result1}"
      puts "Mutable result: #{result2}"

      # Using safe_modify
      result3 = StringHelpers.safe_modify("test string") do |s|
        s.upcase!
        s.reverse!
      end

      puts "Safe modify result: #{result3}"
    end

    helper_demo
  end

  def self.performance_best_practices
    puts "\n=== Performance Best Practices ==="

    # Practice 1: Use string interpolation for dynamic content
    def self.interpolation_practice
      name = "Alice"
      age = 30

      # Good: Creates one new string
      message = "User #{name} is #{age} years old"

      # Less efficient: Multiple concatenations
      message2 = "User " + name + " is " + age.to_s + " years old"

      puts "Interpolated: #{message}"
      puts "Concatenated: #{message2}"
    end

    interpolation_practice

    # Practice 2: Use constants for repeated strings
    module Constants
      ERROR_MESSAGES = {
        not_found: "Resource not found",
        unauthorized: "Access denied",
        invalid_params: "Invalid parameters"
      }.freeze

      def self.get_error_message(type)
        ERROR_MESSAGES[type] || "Unknown error"
      end
    end

    def self.constants_practice
      puts "\nConstants Practice:"

      error_types = [:not_found, :unauthorized, :invalid_params, :unknown]
      error_types.each do |type|
        message = Constants.get_error_message(type)
        puts "#{type}: #{message} (frozen: #{message.frozen?})"
      end
    end

    constants_practice

    # Practice 3: Efficient string building
    def self.string_building_practice
      puts "\nString Building Practice:"

      items = %w[apple banana cherry date elderberry]

      # Efficient: Join with pre-allocated separator
      list1 = items.join(", ")

      # Efficient: String interpolation
      list2 = "Items: #{items.join(', ')}"

      # Less efficient but sometimes necessary: Manual building
      result = +""  # Explicitly mutable
      items.each_with_index do |item, index|
        result << item
        result << ", " unless index == items.length - 1
      end

      puts "Joined: #{list1}"
      puts "Interpolated: #{list2}"
      puts "Manual: #{result}"
    end

    string_building_practice
  end
end

MigrationGuide.common_migration_issues
MigrationGuide.migration_tools
MigrationGuide.performance_best_practices

Advanced Techniques and Edge Cases

✅ Best Practices

  • Enable globally: Use in all new Ruby files
  • Use constants: For repeated string values
  • String interpolation: Preferred for dynamic content
  • Unary plus (+): Create mutable copies when needed
  • Test thoroughly: Verify no string modification issues
  • Monitor memory: Track object allocation improvements

⚠️ Common Pitfalls

  • String mutation: <<, []= and ! methods will fail
  • Default parameters: Hash/Array literals become frozen
  • String buffers: Need explicit mutable creation
  • External libraries: May not be compatible
  • Legacy code: May require significant refactoring
  • Testing gaps: Some mutation bugs may hide

Edge Cases and Advanced Usage

# frozen_string_literal: true

class AdvancedUsage
  def self.demonstrate_edge_cases
    puts "=== Edge Cases ==="

    # Edge case 1: Regexp literals are also affected
    pattern = /hello/i
    puts "Regexp source frozen? #{pattern.source.frozen?}"

    # Edge case 2: Symbol to_s returns frozen strings
    symbol = :test_symbol
    symbol_string = symbol.to_s
    puts "Symbol to_s frozen? #{symbol_string.frozen?}"

    # Edge case 3: Exception messages
    begin
      raise StandardError, "This is an error message"
    rescue StandardError => e
      puts "Exception message frozen? #{e.message.frozen?}"
    end

    # Edge case 4: File operations
    require 'tempfile'
    Tempfile.create('test') do |file|
      file.write("test content")
      file.rewind
      content = file.read
      puts "File content frozen? #{content.frozen?}"
    end
  end

  def self.demonstrate_encoding_behavior
    puts "\n=== Encoding Behavior ==="

    # Frozen strings with different encodings
    utf8_string = "Hello 世界"
    ascii_string = "Hello World".encode('ASCII')

    puts "UTF-8 string: #{utf8_string} (encoding: #{utf8_string.encoding}, frozen: #{utf8_string.frozen?})"
    puts "ASCII string: #{ascii_string} (encoding: #{ascii_string.encoding}, frozen: #{ascii_string.frozen?})"

    # Encoding operations create new strings
    converted = utf8_string.encode('ASCII', invalid: :replace, undef: :replace)
    puts "Converted: #{converted} (frozen: #{converted.frozen?})"
  end

  def self.demonstrate_thread_safety
    puts "\n=== Thread Safety Benefits ==="

    shared_string = "I am shared between threads"
    results = []
    mutex = Mutex.new

    threads = 5.times.map do |i|
      Thread.new do
        # Frozen strings are safe to share
        local_result = "Thread #{i}: #{shared_string}"

        mutex.synchronize do
          results << local_result
        end
      end
    end

    threads.each(&:join)

    puts "Shared string frozen? #{shared_string.frozen?}"
    results.each { |result| puts result }

    # Check if all results reference the same shared string parts
    object_ids = results.map { |r| r.match(/: (.+)$/)[1].object_id }.uniq
    puts "All threads used same string object? #{object_ids.length == 1}"
  end

  def self.demonstrate_memory_profiling
    puts "\n=== Memory Profiling ==="

    # Helper to track object allocations
    def self.track_allocations
      before = ObjectSpace.count_objects
      yield
      after = ObjectSpace.count_objects

      {
        strings: after[:T_STRING] - before[:T_STRING],
        objects: after[:T_OBJECT] - before[:T_OBJECT],
        arrays: after[:T_ARRAY] - before[:T_ARRAY],
        hashes: after[:T_HASH] - before[:T_HASH]
      }
    end

    # Test 1: Frozen string usage
    allocations1 = track_allocations do
      strings = 100.times.map { "constant string" }
    end

    # Test 2: Mutable string usage
    allocations2 = track_allocations do
      strings = 100.times.map { "constant string".dup }
    end

    puts "Frozen strings allocated: #{allocations1[:strings]} objects"
    puts "Mutable strings allocated: #{allocations2[:strings]} objects"
    puts "Memory savings: #{allocations2[:strings] - allocations1[:strings]} objects"
  end

  module StringPool
    # String pool implementation using frozen strings
    @pool = {}

    def self.intern(string)
      frozen = string.frozen? ? string : -string
      @pool[frozen] ||= frozen
    end

    def self.size
      @pool.size
    end

    def self.clear
      @pool.clear
    end
  end

  def self.demonstrate_string_pool
    puts "\n=== String Pool Implementation ==="

    # Add strings to pool
    strings = ["hello", "world", "hello", "ruby", "world", "hello"]

    strings.each { |s| StringPool.intern(s) }

    puts "Original strings: #{strings.length}"
    puts "Pooled strings: #{StringPool.size}"

    # Demonstrate that pooled strings are the same object
    str1 = StringPool.intern("hello")
    str2 = StringPool.intern("hello")

    puts "Same object? #{str1.equal?(str2)}"
    puts "Object ID: #{str1.object_id}"
  end
end

AdvancedUsage.demonstrate_edge_cases
AdvancedUsage.demonstrate_encoding_behavior
AdvancedUsage.demonstrate_thread_safety
AdvancedUsage.demonstrate_memory_profiling
AdvancedUsage.demonstrate_string_pool

🎯 Key Takeaways

  • Memory Efficiency: Identical frozen strings share memory, reducing allocation overhead
  • Performance Benefits: No defensive copying, faster hash key lookups, reduced GC pressure
  • Thread Safety: Immutable strings are inherently safe to share between threads
  • Default in Ruby 3.0+: Enabled by default in new Ruby versions
  • Migration Strategy: Use unary plus (+) for mutable copies, avoid string mutation
  • Best for Constants: Ideal for configuration, templates, and repeated string values
  • Testing Important: Thoroughly test for string modification patterns that might break

Quick Navigation

Related Topics

Video Tutorial

Watch and learn frozen string literals & immutability

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!