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 な文字列に置き換えるなどしていくと良いと思う。

Active StorageとRails 5.2の展望について考える

This separate repository is a staging ground for eventual inclusion in rails/rails prior to the Rails 5.2 release.

Active Storage の Roadmap に書かれている上の引用をどう読むかだけれど、仮に Active Storage を rails/rails リポジトリに含んだうえで Rails 5.2 のリリース機能となると見たとき、Active Storage は Travis CI では Ruby 2.2 を動かしていないことと、Rails 5.2 がリリースされる頃には Ruby 2.2 が EOL もしくはそれに準じた位置づけになっているであろう予測から、Rails 5.2 は Ruby 2.3 以上が動作対象となるということがあり得るのではと考えていたりしていた。

追記 Rails 5.2 は Rails 5.0 / 5.1 同様に Ruby 2.2.2 以上がサポート対象となる。

Bundler 2.0.0.devを使ってみる

Rails の master で Bundler 2 を許可するようになった ので、手元の Rails アプリケーションで Bundler 2 を使ってみることにした。

RubyGems にはまだ Bundler 2 は公開されていないため、ソースコードからインストールする必要があるので、その手順を記しておく (もっとクールなやり方あるかもだけど、他のアプローチは分からない) 。

bundler/bundler の master のソースコードにある version.rb のバージョンを 2.0.0.dev にする。Bundler の実装としてメジャーバージョンが2であれば、Bundler.feature_flag.bundler_2_mode? の値で true を返して Bundler 2 モードなるので、それを有効にするためとなる。

% git diff
diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb
index dad4a88a2..386782023 100644
--- a/lib/bundler/version.rb
+++ b/lib/bundler/version.rb
@@ -7,7 +7,7 @@ module Bundler
   # We're doing this because we might write tests that deal
   # with other versions of bundler and we are unsure how to
   # handle this better.
-  VERSION = "1.15.1" unless defined?(::Bundler::VERSION)
+  VERSION = "2.0.0.dev" unless defined?(::Bundler::VERSION)

   def self.overwrite_loaded_gem_version
     begin

この変更を加えたら rake install を実行する。

% rake install
(snip)
bundler 2.0.0.dev built to pkg/bundler-2.0.0.dev.gem.
bundler (2.0.0.dev) installed.

ソースコードへの変更で指定したバージョンが表示されれば成功。

% bundle -v
Bundler version 2.0.0.dev

Bundler 2 でのインパクトが大きい変更のひとつと思っているのは Gemfile と Gemfile.lock といったファイル名が非推奨となり、それぞれ gems.rb と gems.locked というファイル名になる点。Bundler 2 では非推奨警告が出るにとどまり、Gemfile と Gemfile.lock がなくなるのは Bundler 3 のタイミングの模様

% bundle install
DEPRECATION: Gemfile and Gemfile.lock are deprecated and have been replaced with gems.rb and gems.locked. Please use them instead.

Ruby のサポートについても、Ruby 2.3 以上にしようかなど話が進んでたりしている (良い話) 。

他にも bundle update は GEM 名の指定がない場合、つまり Bundler 1 系での振る舞いとなるすべての Gem を bundle update したい場合は bundle update --all とするように変更されたり、bundle install でのデフォルトのインストールパスが .bundle/gems に変更されたりなどの Breaking Changes がある。

細かいところでは bundle の挙動が bundle install ではなく、サブコマンド一覧の表示になったり違和感のない動きになったりしている。

そのほか、Bundler 2 の今後の動向については RFC などウォッチしてみると良いと思う。

github.com