When working with Ruby programs, you must have come across the following comment at the top of the file:
# frozen_string_literal: true
print "Let's learn Ruby on Rails"
# frozen_string_literal: true
is a magic comment in Ruby, which tells the Ruby interpreter that all the string literals must be frozen and memory should be allocated only once for each string literal.
A magic comment is a special type of comment in Ruby that's defined at the top of the Ruby script. It affects how the Ruby code is executed. You can think of them as "directives" or "instructions" for the Ruby interpreter.
Magic comments must always be in the first comment section in the file. Their scope is limited only to the Ruby script in which they appear. You must put the magic comment in each file where you want the special behavior.
That was easy. Just add the magic comment at the top, and all string literals will be frozen. But what does it mean? Why do you need to freeze a string in the first place? What are the benefits? What are the costs?
Let's learn more.
What does it mean to freeze a String?
It's funny, I've been programming Ruby for the past two years and I had no idea what freezing did (or not do) in Ruby. So while researching this post, I decided to really learn it. Here is everything I know about freezing, so far.
So it turns out, a constant is not really a constant in Ruby...
LANGUAGE = "Ruby"
LANGUAGE << " on Rails"
puts LANGUAGE # Ruby on Rails .. Whaaaat???
... unless you freeze it, by calling the freeze
method on the object.
LANGUAGE = "Ruby".freeze
LANGUAGE << " on Rails"
puts LANGUAGE # can't modify frozen String: "Ruby" (FrozenError) ... whewww
Calling freeze
turns the string into a real constant. And if you try to modify it, Ruby will raise the FrozenError
. Does that make sense? To prevent modifications to a string, we had to call the freeze
method on it.
Let's read up the docs on the freeze
method.
Object.freeze
Prevents further modifications to
obj
. A
FrozenError
will be raised if modification is attempted. There is no way to unfreeze a frozen object.
Calling freeze
makes an object immutable, that is, its internal state cannot be changed. That means when a string is frozen, you cannot use the bang! version of methods on it, which modify the original strings.
framework = "Ruby on Rails".freeze
puts framework.upcase # RUBY ON RAILS
puts framework.upcase! # can't modify frozen String: "Ruby on Rails" (FrozenError)
Not only the freeze
method works with a string, it also work on Arrays and Hashes (and a few other type of objects, but let's ignore them for now).
languages = [ "Rub", "C-Sharp" ].freeze
# can't modify frozen Array: ["Rub", "C-Sharp"] (FrozenError)
languages << "Java"
rails = {
language: "Ruby",
creator: "DHH"
}.freeze
# the `update` method modifies self, like merge!
# can't modify frozen Hash: {:language=>"Ruby", :creator=>"DHH"}
rails.update(website: "https://rubyonrails.org/")
So one thing is clear. Freezing an object literally freezes it, you cannot modify it.
To check if an object is frozen or not, simply call the frozen?
method on it.
language = "Ruby"
p language.frozen? # false
framework = "Ruby on Rails".freeze
p framework.frozen? # true
The next question you might have (I certainly did): Can we unfreeze an object?
The answer is NO. You can not unfreeze an object that is already frozen.
However, you can make a copy of the frozen object which will be unfrozen.
framework = "Ruby on Rails".freeze
p framework.frozen? # true
new_framework = framework.dup
p new_framework.frozen? # false
To unfreeze a string, you can also use the +string
, which returns the same string if it's not frozen. Otherwise, it calls the dup
method on string and returns a new string which is not frozen.
framework = "Ruby on Rails".freeze
p framework.frozen? # true
new_framework = +framework
p new_framework.frozen? # false
# By the way, did I mention the `-` operator which freezes a string?
# I think it's too much syntactic sugar, though. What do you think?
framework = -"Ruby on Rails"
puts framework.upcase! # can't modify frozen String: "Ruby on Rails" (FrozenError)
Alright, so far we have learned that frozen objects cannot be modified.
The obvious use for freezing is to prevent unintended modifications to sensitive objects (and also to prevent the number of object allocations, which we'll learn later). However, it can be cumbersome to having to remember and call the freeze
method every time you want to create an unmodifiable object.
Wouldn't it be nice if there was a way to do it automatically? It would.
The frozen_string_literal
allows you to freeze all string literals by default, in a Ruby script.
Update: Last month, Matz accepted the proposal to have the
frozen_string_literal: true
enabled by default."I agree with the proposal. It seems a well-thought process to migrate. The performance improvement was not as great as I had hoped for. But since I feel that the style of individually freezing strings when setting them to constants is not beautiful, and since I feel that magic comment is not a good style. I feel that making string literals frozen is the right direction to go in the long run." - Matz
Let's rewrite the same code above, with the magic comment in place.
# frozen_string_literal: true
framework = "Ruby on Rails"
p framework.frozen? # true
As you can see, the string was frozen without us having to call the freeze
method on it. However, it won't freeze other types of objects, such as arrays and hashes. It will only freeze strings.
# frozen_string_literal: true
framework = "Ruby on Rails"
p framework.frozen? # true
lanugages = [ "Ruby", "Java" ]
p lanugages.frozen? # false
h = { language: "Ruby" }
p h.frozen? # false
So far, we've learned that the magic comment frozen_string_literal: true
will freeze the strings and prevent them from modifications. That's nice.
However, there's another important reason for freezing strings, which has to do with improving the performance of your application, by avoiding the number of objects the Ruby interpreter has to create and allocate memory for.
Let's dig in to learn more.
Reducing Memory Allocations for String Literals
First, let's take a detour to understand the Object#object_id
method. When called on an object, it returns a number.
puts "Ruby".object_id # 60
puts ["Python", "Java"].object_id # 80
However, it's not any ordinary number. No two active objects will share an ID. To keep things simple, you can think of object_id
as that object's location in memory (if you're a Ruby expert, please correct me if this mental model is wrong).
If you call object_id
multiple times on the same object, you will get the same number. However, calling object_id
on different objects will return different numbers.
ruby_one = { name: "Ruby", creator: "Matz" }
puts ruby_one.object_id # 60
puts ruby_one.object_id # 60
ruby_two = { name: "Ruby", creator: "Matz" }
puts ruby_two.object_id # 80
puts ruby_two.object_id # 80
Even though the object structure was same, the Ruby interpreter created a new object for ruby_two
and allocated new memory to it.
I hope that by now you understand how object_id
works. Now let's get back to strings.
Consider the default behavior of Ruby strings, when they are not frozen:
puts "Ruby on Rails".object_id # 60
puts "Ruby on Rails".object_id # 80
puts "Ruby on Rails".object_id # 100
Note that the object ids of three strings are different. What that means is the Ruby interpreter created a new string object whenever it came across the string "Ruby on Rails"
, even though it's one string.
Now imagine that you're using the same string literal hundreds or thousands of times in a loop. The interpreter will create a new object and allocate memory to it in each iteration of the loop.
5.times do
puts "Ruby on Rails".object_id
end
# Output
60
80
100
120
140
That's a lot of overhead. It is quite inefficient.
Why? Because Ruby is a garbage-collected language.
The more memory is allocated for all these objects, the more work the garbage collector needs to do. Which sucks, as all the time Ruby interpreter spends collecting the garbage (which means "reclaiming the allocated memory"), it cannot spend executing your code.
Let's run the same code again, this time with the magic comment.
# frozen_string_literal: true
puts "Ruby on Rails".object_id # 60
puts "Ruby on Rails".object_id # 60
puts "Ruby on Rails".object_id # 60
5.times do
puts "Ruby on Rails".object_id
end
# Output
60
60
60
60
60
60
60
60
As you can see, when the string literal "Ruby on Rails"
was frozen (because we used the magic comment), the Ruby interpreter did not create new objects for the same string. That means, the same object was reused, saving the space for two additional strings. Hence the garbage collector's work was significantly reduced.
That's a big reason you see the magic comment # frozen_string_literal: true
in most Ruby projects. For example, the Ruby on Rails project uses it almost everywhere.
Let's test this by running some numbers with the benchmark-ips
gem, which benchmarks a given blocks number of iterations per second.
require 'benchmark/ips'
def learn(obj)
end
frozen_rails = "Ruby on Rails".freeze
Benchmark.ips do |x|
x.report("plain") { learn "Ruby on Rails" }
x.report("frozen") { learn frozen_rails }
end
# Output:
ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-darwin21]
Warming up --------------------------------------
plain 1.235M i/100ms
frozen 1.972M i/100ms
Calculating -------------------------------------
plain 12.335M (± 5.2%) i/s - 61.774M in 5.022481s
frozen 20.203M (± 4.5%) i/s - 102.564M in 5.087328s
As you can see, the frozen version of the block is significantly faster (102.6 million iterations per second) than the plain one (61.7 million iterations per second). No wonder most Ruby projects freeze strings everywhere.
Note: It's important to remember that the scope of # frozen_string_literal: true
is limited only to the current file. You must put it in each file where you want the intended behavior.
I wish there was some sort of global switch that turned it on everywhere. Although, there is a –enable frozen-string-literal
flag that you can use while running Ruby scripts, which is kind of nice.
That's it for today.
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.