Jake Worth

Frozen String Comment: YAGNI

Published: February 09, 2022 4 min read

  • ruby

Open a production Ruby file, and you’ll often see this magic comment at the top.

# frozen_string_literal: true

Today I’d like to argue that most Ruby files do not need this comment. You aren’t going to need it (YAGNI) and the norm should be exclusion.

Why? I believe it is a relic of the past, it doesn’t do what people think, it’s not Ruby-ish, and it has a questionable impact on performance. I’d like to see it deleted from the code I work on, and I don’t think we should be enforcing its presence it most of the time.

So, what is a magic comment? Here’s an explanation from the Ruby docs:

While comments are typically ignored by Ruby, special “magic comments” contain directives that affect how the code is interpreted.

This particular magic comment, however, has a unique history.

It’s a Relic

“All String literals are immutable (frozen) on Ruby 3.” —Ruby creator Matz

The frozen_string_literal magic comment was added to Ruby to prepare codebases for frozen strings being the default in Ruby 3. Ruby 3 is now available, and we know this did not come to pass. What happened? Matz changed his mind:

“I consider this for years. I REALLY like the idea but I am sure introducing this could cause HUGE compatibility issue, even bigger than Ruby 1.9. So I officially abandon making frozen-string-literals default (for Ruby3).” —Matz

Think of this comment as a shim to prepare your codebase for a future change that will never happen. It’s a deprecation warning that is not ever going to be resolvable. It’s a relic.

It’s Misunderstood

Even worse, this magic comment is frequently misunderstood.

The comment behaves as if .freeze had been called on each string. A common understanding of .freeze is that it makes the string immutable. Let’s look at that.

Here’s some Ruby code to demonstrate freezing. We’ll create a string, .freeze it, verify it is frozen, and read the object ID.

irb> frozen = "frozen"
=> "frozen"
irb> frozen.freeze
=> "frozen"
irb> frozen.frozen?
=> true
irb> frozen.object_id
=> 70348222862920

So far, so good. Here’s what many Rubyists, including me recently, think you can’t do to this frozen string: addition assignment.

> frozen += " like ice"
=> "frozen like ice"

Whoops! Didn’t we just append to an immutable object? How is that possible? It’s possible because it’s a new object.

> frozen.object_id
=> 70348222814640 # was 70348222862920

So what can’t we do? We can’t append to the string:

> frozen.freeze
=> "frozen like ice"
> frozen << " and snow"
RuntimeError: can't modify frozen String

When it comes to modifying strings, my experience is that += is much more common than << or .concat. Such that I’ve never even seen this runtime error before.

If this was just one person’s misunderstanding, okay. However, I’ve heard people use addition assignment to explain this feature many times. It’s how people think the feature ought to work. That’s a problem.

It’s Not Ruby-ish

It is surprising to me when comments in Ruby are evaluated. I feel that magic comments detract from the readability of a Ruby file, because I have to know something unusual to read the code. Rubyists expect comments to be unevaluated documentation.

I found the unusual syntax more palatable when it was a shim. Now that the language has committed to a different direction, why hold onto it?

Preemptive Performance

A common argument in support of this comment is that it creates more performant code. That’s a claim we need to question.

This post from Honeybadger includes a benchmarking script that demonstrates a measurable performance gain when using .freeze. I’ve copied it here:

# test.rb
require 'benchmark/ips'

def noop(arg); end

Benchmark.ips do |x|
  x.report('normal') { noop('foo') }
  x.report('frozen') { noop('foo'.freeze) }

Here’s the result of this script when I ran it on Ruby 2.4:

$ ruby test.rb
Warming up --------------------------------------
              normal   755.202k i/100ms
              frozen   932.701k i/100ms
Calculating -------------------------------------
              normal      7.596M (± 0.8%) i/s -     38.515M in   5.071109s
              frozen      8.895M (± 0.7%) i/s -     44.770M in   5.033199s

Unfrozen completes 7.596M iterations per second, while frozen completes 8.895M iterations per second. That’s statically significant! Does that mean we should use .freeze all the time?

It depends. I believe that performance can be an ‘Appeal to Common Belief’ fallacy. The winds are against you anytime you argue against it. As a result, many premature, inscrutable programming decisions have been made in performance’s name.

Optimizations like this need to prove that they matter. Does decorating your code with magic comments make a difference? We must prove it, because every choice has tradeoffs. In this case, requiring the comment introduces the following tradeoffs: wasted space, lower readability, unearned confidence that the codebase is extra performant, failing CI builds when it is required but absent, and more.

Let’s stick go solving performance problems that we definitely have. If you’ve decided it really does matter, run your program with the CLI flag (thanks Dillon):

$ your_process.rb --enable-frozen-string-literal

It’s Everywhere

This comment is very prevalent in Rails apps. Why is that? Rubocop. The popular Ruby linter enforces this feature without prejudice.

If you don’t care about the comment, and are instead trying to appease the linter, why not achieve the same by just ignoring it? Disable that cop:

# .rubocop.yml
  Enabled: false

Wrapping Up

I’m grateful to the Ruby team for proposing this optimization. Enhancing performance while keeping Ruby Ruby is a noble pursuit.

I believe that this comment is deletable most of the time. Consider the value this comment brings to your code. If I’m wrong, I’d love to hear why.

✉️ Get better at programming by learning with me. Subscribe to Jake Worth's Newsletter for bi-weekly ideas, creations, and curated resources from across the world of programming. Join me today!

Blog of Jake Worth, software engineer in Maine.

© 2022 Jake Worth.