53
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RubyAdvent Calendar 2022

Day 8

【Ruby】# frozen_string_literal: trueマジックコメントは必要?【RuboCop】

Last updated at Posted at 2022-12-07

最初に

RuboCopの全てのCopをオンにすると、
Style::FrozenStringLiteralCommentという項目があります。

デフォルトだと基本的に全てのRubyファイルの冒頭に
# frozen_string_literal: trueを書くよう注意されます。

p 1 + 1と何か書くだけでも、マジックコメントを書くよう要求されます。

厳しいと感じる項目は他にもあるのですが、
全てのファイルに要求されるのでとりわけ異質に厳しく感じています。

このマジックコメントって何をしているのでしょうか。
本当に必要なんでしょうか。
オフにしても良さそうな設定でしょうか。

自分の結論ではこのマジックコメントを使いたくなく、
そういう結論になった背景について備忘としてまとめます。

注意

Railsアプリにも言えるだろうと思っていますが、
Railsアプリに詳しくないのであまり念頭には置いてないです。
どちらかといえば、書き捨てのコード・小さなgemです。

# frozen_string_literal: trueの意味から考える

このマジックコメントが書かれているファイル上では、
文字列リテラルを最初からfreezeした状態で生成します。
freezeした状態とは、イミュータブル(破壊的変更が不可)を意味します

つまり、文字列リテラルがなければ、
そのファイルにマジックコメントを書いたところで何も機能してないです。

機能しない1行を入れるのって、無駄ではないでしょうか?
文字列を使わないファイル上で、自分は意味のない機能を書きたくないです。

# frozen_string_literal: trueの本来の目的

このマジックコメントがRubyに導入された背景を考えてみます。

まだRuby2.xの時代の頃に
「Ruby 3.0では、文字列をイミュータブル(frozen)で生成する!!」という計画がたちました。

この計画はなくなり今に至りますが、
計画が立ったその時期に本題のマジックコメントは導入されました。

当時の状況は、次のブログ記事に詳しく書かれています。

[Ruby] Ruby 3.0 の特大の非互換について - まめめも

いきなりこのように変えると互換性問題がひどすぎるということで、移行パスとして、Ruby 2.3 に frozen_string_literal というマジックコメントが導入される予定です。すでに trunk には導入されています。(Feature #8976: file-scope freeze_string directive)

matz は「immutable string literal のアイデアを検証するためにマジコメを入れる」と言っているので、

この引用を見ると、当該マジックコメントは非互換への移行過程のために導入されてます。

ですが、ご存知のように、文字列イミュータブル計画は半永久的に中止になり、
現在のRuby 3.1でも文字列はミュータブルなまま生成されます。

本来の目的は失われたわけですから、
本来の目的から見るとこのマジックコメントは不要なように思えます。

パフォーマンスについて

RuboCopのコアはパフォーマンス向上を目的としてないはずですが、
念の為にパフォーマンスがどうなるかを検証していきます。

このマジックコメントが導入されたそもそもの背景には、
Railsのパフォーマンスが関わっていますし。

多面的じゃなくフェアじゃないかもしれませんが、簡単に計測していきます。

文字列の生成時間の差は、大したことなさそう

# frozen_string_literal: trueで、文字列を生成したところで、
どれぐらいパフォーマンスが変わるのでしょうか?

次のようなコードで、マジックコメントを消したりして生成時間の差を計測しました。

# frozen_string_literal: true
require 'benchmark'
n = 10**8
Benchmark.bm(0) do |r|
  r.report{ n.times{ 'abcdefghijklmnopqrstuvwxyz' } }
end

ループ回して1億回の生成で、2倍ほどの差がでました。
多くのケースでは、たった2倍だと思います。

             user     system      total        real
mutable  5.176078   0.000000   5.176078 (  5.176290)
frozen   2.481366   0.000784   2.482150 (  2.482255)

1億回の生成で2.5 ~ 3.0秒の差です。
文字列の生成のせいで、遅さを体感することって少ないように思います。
(ケースバイケースですけどね)

ハッシュのキーのアクセスについても検証し、
イミュータブルな方が1.2倍ぐらい速かった程度で、
大きな差はなかったので割愛します。
なお、これは、シンボルをハッシュのキーにした方が2倍ぐらい速かったです。

逆に破壊的変更が使えずに遅くなるケース

反対に、文字列の破壊的変更ができないと遅くなるようなケースはないのでしょうか?

あります。

少し特殊かもしれませが、末尾への追加として+=を利用した再代入が遅いです。

require 'benchmark'
n = 200_000
s = ''
t = ''
Benchmark.bm(2) do |r|
  r.report('+='){ n.times{ s += 'a' } }
  r.report('<<'){ n.times{ t << 'a' } }
end

p s == t

これを計測してみますと、わずか20万回で100倍以上の差がでます。

         user     system      total        real
+=   3.170704   0.748027   3.918731 (  3.918978)
<<   0.017864   0.000000   0.017864 (  0.017878)

形式的に文字列の破壊的変更を封じられると、
末尾に追加する<<が使用できず、パフォーマンスの悪いコードになる可能性があります。

パフォーマンスに関してのまとめ

比較方法がフェアじゃないかもしれませんが、
破壊的変更を忌避しすぎるとパフォーマンスが劇的に悪化する危険性があります。

もし「常に速くなるから# frozen_string_literal: trueをつけて、文字列の破壊的変更を封じておこう」という考えであれば、間違いなはずです。

文字列の破壊的変更を封じておくといいことがある?

破壊的変更を封じておくと、良いことはあるのでしょうか。

まぁ破壊的変更をされたくない文字列があったとして、
破壊的変更をされようとすると直接エラーになるので、
バグを生みにくいメリットはあるとは思います。

文字列リテラルを使っているケースにおいては、このメリットはあるとは思います。

ただ、やっぱり文字列リテラルがないスクリプトファイルに
最初から全てのファイルにこのマジックコメントを書いておくのは変に思います。

特に、もし文字列リテラルの登場が少ないGem等に、
このマジックコメントがあらゆるファイルにあったりすると自分は変に感じてしまいます。
いちいちおまじないを書くのは、Rubyの良さを殺している感じがします。

ところで、RuboCopのStyleの項目とは?

話が少し逸れるかもしれませんが、RuboCopについて見ていきます。

Styleという項目の中に当該マジックコメントを強制させるCopがあります。
しかし、自分の中でのStyleにあるCopのイメージは「どっちの(どの)スタイルを選んでもコードの機能・挙動は変わらないが、スマートなスタイル(書き方)を提供したり、どちらかのスタイル(書き方)に統一したりするもの」という感じです。

当該マジックコメントは破壊的変更という機能を封じるという意味で、機能を変更してしまうのでStyleぽくない感じがします。
また、無から(意味のないところにも)マジックコメントを書くことを推奨してくるので変な感じがします。

ただ、ここでドキュメントを読むと、マジックコメントにも以下のスタイルがあることがわかりました。

  • Copを有効に
    • always : デフォルト。全ファイルにマジックコメントを書く。falseとしても良い。
    • never: 絶対にマジックコメントを書いてはならない。
    • always_true: 全てのファイルに常にtrueでマジックコメントを書く。
  • Copを無効に
    • Enabled: false: 機能させない。自由。

機能させないパターンも含んで、現在4通りありました。

RuboCopのドキュメントを読んで、デフォルトのalwaysはマジックコメントでfalseと書いてもいいのだと初めて知りました。
しかし、ふつうtrueが書いてあるという前提が無意識に働き、もしfalseと書いあっても読み飛ばしてしまいそうなので、その点は注意ですね。

自分が百歩譲ってあってもいいかな・あった方が良さそうと思うのは、
「文字列リテラルが存在するファイルにだけ、マジックコメントを書く」というスタイルです。

そうすれば、無意味なマジックコメントが書かれているという違和感がなくなり、
自分の溜飲が少し下がる気がします。

まとめ

長くなってしまったので、適当にまとめます。

  • 全てのファイルに書いたところで、文字列リテラルのないところでは機能していない。
  • 「互換性の問題を和らげる」というマジックコメントの本来の目的は失われている。
  • パフォーマンスが良くなるというが、そもそもボトルネックになるほど遅いかな?
  • 反対に、破壊的変更が封じられると、めちゃくちゃ遅くなるケースもある。
  • 文字列の誤った破壊的変更を防ぐ意義はあるとは思う。
  • いちいちおまじないを書くのは、Rubyの良さを殺している感じがする。
    • 見栄えが悪い。自分の中では、すべての文字列に.freezeつけるよりはマシなだけ。

各ファイルの冒頭にたった1行といえば1行ですが、自分的にはおまじない的な感じで全てのファイルに書かされている感じがイヤなので、真っ先にオフにしたいCopです。個人的には。
色々思った些末なこと・書きたいことは他にも多々あるのですが、このあたりにしておきます。

以上です。

53
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?