Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: Symbol#to_sはRuby 2.7 previewでfrozen Stringを返したが今は違う(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: Symbol#to_sはRuby 2.7 previewでfrozen Stringを返したが今は違う(翻訳)

壊れたインターフェイスが修復されたことで壊れていたことを知る

私がRubyを愛してやまないポイントのひとつは、言語設計のさまざまな側面についてさまざまな方向から注目を集めるやり方です。Ruby言語の変更はJRuby界隈からやってくることがよくあります(Charles Nutter氏からのFeature #16150など)。TruffleRubyを現在率いているeregonことBenoit Daloze氏も主要なコメ主のひとりです。言うまでもありませんが、今もRuby言語の主要な言語設計者であるMatzを含むCRuby界隈の人々もそうです。

このバグにはいくつか興味深い影響がありますので、それについて少しばかり語ってみたいと思います。あるインターフェイスが当初十分練り上げられていなかったからといって、後の修正が困難になるとは限りません。別にto_sを槍玉に挙げようというのではありません。to_sはほとんどの場合それなりによいインターフェイスです。しかしRubyでは、当初ささやかだったものはことごとく、言語が成熟するにつれて多くのユーザーに使われるようになります。どんなインターフェイスでも、利用法が変わったり利用者が増加するに連れて、多少なりとも問題を抱えるようになるものです。本記事で申し上げるのは、そうした多くのよい例の中のほんの一例に過ぎません。

「で、どこがどう変わったの?」

ご存知のように、Rubyのあるオブジェクトでto_sを呼ぶと、そのオブジェクト自身をstringに「変換したもの」が返ることが期待されます。たとえば数値の7to_sを呼べば文字列の"7"が返りますし、:bobのようなシンボルでto_sを呼べば"bob"というstringが返ります。文字列の場合は、何も変更せずに自分自身を直接返します。

Rubyには他にもto_ato_hashto_fto_iといった「型変換」メソッドがひととおり揃っています。さらにややこしいのが、ほとんどの型に型変換演算子が1つではなく2つあることです。stringに変換するメソッドにはto_sto_strがありますし、arrayならto_ato_aryといった具合です。これらの演算子や、その他の型変換方法、それらがどう使われているかについて詳しく知りたい方には、Avdi Grimm氏の良書『Confident Ruby,』を強くおすすめしておきます。この書籍は普通に購入することも、「はがき」を送って'引き換え'にすることもできます。とにかく今は私を信じてください。この手の「型変換演算子」は山ほどあり、to_sはその中のひとつに過ぎません。

Ruby 2.7-preview2はRubyのランダムなプレリリース版のひとつですが、このときからSymbol_to_sは変更できないfrozen stringを返すようになりました。これによっていくつかのコードが動かなくなりますが、それがまさにこの変更で私が踏み抜いた部分なのです。私はRubyで書かれた割と前のスピードテストを定期的に動かしていますが、このテストには私が踏んだ小さな潜在的問題がたくさんあります。

これが問題である理由

この問題はいつ起きるのでしょう?誰かがto_sを呼んだだけでほとんどの結果が台無しになります。以下は私がつまづいたコードですが、これは古いActive Supportを元にしています。

    def method_missing(name, *args)
      name_string = name.to_s
      if name_string.chomp!("=")
        self[name_string] = args.first
      else
        bangs = name_string.chomp!("!")

        if bangs
          self[name_string].presence || raise(KeyError.new(":# is blank"))
        else
          self[name_string]
        end
      end
    end

「これは完璧な方法なのに、それが新しい変更で壊れたの?」と思いますか?そぉぉぉぉなんです!実に、実にいい質問です!(Q1)

少なくとも当時の私が即答できそうになかった「いい質問」は他にもあります。

  • Q2: stringは普通string自身を返すんだとしたら、受け取ったstringを改変すると元のstringも変わるの?
  • Q3: 毎回新しいstringをアロケーションするのは最適化で問題になるの?(Schneems氏はこの問題を#34197で回避せざるを得なくなりました)

これらはいずれも難問です。Q1を明示的に修正すればおそらくQ2がぶっ壊れますし、逆もしかりです。しかもQ3はぞっとします。果たしてこの振る舞いを部分的に停止してよいものでしょうか?Rubyでは可能ですが、気にするべきなのはそこではありません。

本記事冒頭で、to_sというインターフェイスは「練り上げが完璧ではなかった」と書きました。言いたかったのはそこです。to_sは「うまくやれる場合もある」限定的なインターフェイスであり、それが使われるコンテキストについて単に十分考察されていなかったのです。これはどんなインターフェイスでも同様で、「これまで想像もしなかった新しい利用法、新しいコンテキスト、新しい応用が出現する」か、「元の設計が間違っている」か、そのどちらかになるのです。

間違っている」なんて書くのは強すぎでしょうか?そうとも限りません。Charles Nutter氏は#16150のコメントで、現在の設計は私たちの利用法において単純に「安全でない」と指摘しています。結果を改変したらどうなるかが保証されておらず、改変が合法かどうかを決められる保証もありません。実際、人々は結果を改変しているのです。仮にみんなが結果を改変していなければ、安全と最適化のために結果をちょいとfreezeしたとしても誰ひとり気が付かないでしょう(詳しくは後述)。

そしていつの日か、変換メソッドは(to_sに限らず)一般に結果の改変が安全でないことにも気づくでしょう。to_sだけを犯人扱いするのは疑問です。

「三人寄れば文殊の知恵」、そして現実的な答え

Ruby 2.7について言えば、答えは既に出ています。Symbol#to_sがfrozen stringを返すと一部のコードが壊れます。特に「どこが壊れるか」については#16150のコメントで6つほどリストアップされていて、古いものもあればはっきりしないものもあるようです。しかしそれこそpreview版で明らかにしようとしていることですよね?この変更が問題になることが2.7の最終リリースまでに判明すれば、ロールバックは簡単です。これまでもそうしたことはありましたし、今後もあるでしょう。

(これについては実際このとおりになりました。2.7.0リリース版にはこの変更は含まれておらず、この機能については現在再検討中です。この変更は今後再び入るかもしれませんし、形を変えて入るかもしれません。実際にRubyコアチームは、実現可能な後方互換性の維持に努めています。)

それまでは、to_sで呼び出した結果を改変しようとしているそこのあなた、それは改変しないことをおすすめします。言語のその部分が後で壊れるかもしれない(壊れないかもしれない)というだけではなく、今後も動作する保証がないことがわかっているからです。一般に、変換メソッドで複製したオブジェクトの結果が改変可能であると信じてはいけません。結果がfrozenかもしれませんし、悪くすると元のオブジェクトを改変してしまう可能性が無きにしもあらずです。さらに言えば、この改変には保証がありませんし、今できているからといって今後もやれるという保証もないのです。

かくして進歩によって新たな問題が発生し、私たち一同はインターフェイスの設計についてささやかな教訓を得ることになったのです。

関連記事

Rails: Active Recordメソッドのパフォーマンス改善とN+1問題の克服(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。