Magic comment ‘immutable: string’ makes Ruby 2.1’s “literal”.freeze optimization the default

In Mutable strings in Ruby we saw that making Ruby strings immutable with .freeze can remove a common source of bugs.  We also saw that Ruby’s allowance for string mutation can cause significant performance degradation because string literals must be allocated (and later garbage collected) every time that code is run.

Ruby 2.1 takes a step towards addressing the performance problem with its “literal”.freeze (formerly “literal”f suffix) optimization. This addresses the performance problem described in Part 1, when you write ‘.freeze’ after your string literals:

def log_message(message)
  puts message + "[EOL]".freeze
end

The above change can make a big difference in performance.  But you have to remember to put ‘.freeze” after every string literal, which is ugly and impractical to do broadly.

# -*- immutable: string -*- Makes “literal”.freeze the Default

To be practical, we need a way to mark entire code files as having their string literals as frozen by default.  This pull request does just that with a magic ‘immutable: string’ comment at the top of the file.  Here’s a test that shows its usage:

# -*- immutable: string -*-

require 'minitest/autorun'
require_relative 'test2.rb' # file with a mutable string

heredoc_string = <<-EOS
  Hello World
EOS

strings = [
  "hello",
  %{Hello},
  %Q{Hello},
  %q{Hello},
  heredoc_string
]

def mutate(str)
  str.slice!(1, 2)
end

def log(message)
  "I'm logging: #{message}"
end

describe "strings defined in this file" do
  strings.each do |s|
    it "should raise an error" do
      -> {
        mutate(s)
      }.must_raise(RuntimeError).message.must_match(/can't modify frozen String/)
    end
  end
end

describe "string interpolation" do
  it "should fail" do
   -> {
      str = log("blah blah")
      mutate(str)
    }.must_raise(RuntimeError).message.must_match(/can't modify frozen String/)
  end

  it "should succeed" do
    -> { str = "foo#{some_string}" }
  end
end

describe "strings not defined in this file" do
  it "should be mutable" do
    mutate(Foo::CONSTANT)
    Foo::CONSTANT.must_equal "SING"
  end
end

describe "static strings" do
  it "should always have the same object_id" do
    def some_string
      "A nice frozen string!"
    end
    some_string.object_id.must_equal some_string.object_id
  end
end
# test2.rb
module Foo
  CONSTANT = "STRING"
end

Overriding immutable: string

Sometimes in a file with the magic comment you may actually need a mutable string.  That is easy to do with String.new or ”.dup:

# -*- immutable: string -*-

def concatenate(*args)
  result = String.new     # or:  result = ''.dup
  args.each do |arg|
    result << arg
  end
  result
end

Making Ruby More Functional

This new magic comment should allow some big performance gains and lessen bugs at the same time, taking Ruby one step closer to Functional Programming.  We sincerely hope this pull request will be accepted into the main Ruby 2.1 branch.

Questions or comments?  Feel free to post them here, email colin@invoca.com, or tweet to @colindkelley.

2 thoughts on “Magic comment ‘immutable: string’ makes Ruby 2.1’s “literal”.freeze optimization the default

Leave a Reply

Your email address will not be published. Required fields are marked *


× 3 = twenty four