frozen string literalへのデバッグオプション

Rails 本体への frozen string literal への導入がはじまっているようなので、Rails アプリケーション側についても頃合いがやってきていると個人的に思っているこの頃。frozen string への破壊的操作があった際に起きる can't modify frozen String (RuntimeError) について、バックトレースだけでは frozen string を生成している場所を見つけづらいことがある。

そういったときに使えるのが --debug=frozen-string-literal オプション。

以下のような frozen_string_literal_true.rb というファイルがあるとする。

# frozen_string_literal: true

'foo' << 'bar'

オプションなしで実行した場合は以下のようなエラーメッセージとなる。

% ruby frozen_string_literal_true.rb
Traceback (most recent call last):
frozen_string_literal_true.rb:3:in `<main>': can't modify frozen String (RuntimeError)

--debug=frozen-string-literal オプションつきで実行した場合は、以下のように frozen string を生成した場所がデバッグ情報として付加される。

% RUBYOPT=--debug=frozen-string-literal ruby frozen_string_literal_true.rb
Traceback (most recent call last):
frozen_string_literal_true.rb:3:in `<main>': can't modify frozen String, created at frozen_string_literal_true.rb:3 (RuntimeError)

さすがにこの例だと、文字列を生成した場所と破壊的操作をした場所が同じなのでありがたみが伝わらないと思うが、それらが離れているときに効率よくデバッグをすることができる (実践的には RSpec などの実行コマンドに RUBYOPT=--debug=frozen-string-literal を付加して、frozen string の生成場所を調べることになる) 。

ちなみに Ruby 2.2 以下のサポートが必要なライブラリではなく、動作対象を Ruby 2.3 以上に絞ったものの場合は String#dup よりも String#+@ を使った方がパフォーマンスが良いと先日 RuboCop にマージされた Performance/UnfreezeString cop で知った。

github.com

そのあたりも含めて can't modify frozen String の発生元を開発効率よく探して、mutable な文字列に置き換えるなどしていくと良いと思う。