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.
Nice summary of Ruby 2.1 frozen strings by Aman Gupta, including a shout-out for the immutable: string magic comment we are proposing: http://tmm1.net/ruby21-fstrings/
With the ‘immutable: string’ magic comment, this benchmark runs 1.6X faster (that is, in just 61% of the time) compared to stock Ruby 2.1.0!
https://gist.github.com/ColinDKelley/8156708