Rubyのシンボルをなくせるか考えてみた(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Could we drop Symbols from Ruby? 原文公開日: 2017/10/02 著者: Robert Pankowecki サイト: Arkency Rubyのシンボルをなくせるか考えてみた(翻訳) この記事を読んでいる皆さまの場合はわかりませんが、私は個人的に文字列とシンボルの区別に由来するバグを余裕で10回以上は踏んでいます。このバグは私の書いたコードでも、他のライブラリを使うときにも起きました。私はコードに表示されるシンボルの外観は好きなのですが、シンボルと文字列の特定の区別のされ方が好きではありません。(こういうことを書くと炎上しそうですが)この区別は問題を解決するよりも作り出す方が多いと思います。 そこで私も考えてみました。いっそシンボルをなくしてしまえばどうだろう。過激な主張でしょうか?かといって、何千というRubyライブラリを書き直して、そこに含まれる:symbolを片っ端から削除して回るという戦略に勝ち目があるとは思えません。おそらくシンボルリテラルはfrozenかつイミュータブルな文字列として使うこともできるでしょう。どのような仕組みになっているのでしょうか。 もしも世界がこうだったら 問題の解決法を長いパラグラフでみっちり記述するのはつらいので、私が空想する性質をデモでお見せして、コードそのものに語らせたいと思います。もしもの世界へようこそ。 :foo == :foo # true :foo == “foo” # true これが私の出発点であり、目指すゴールでもあります。文字列かシンボルかという区別はもううんざりです。もちろんこんな簡単な話ではありません。私の空想する動作を完全に説明するにはもっと多くの性質(テストケース)が必要です。 私のユースケースでは多くの場合、ハッシュからの値の取り出しやハッシュへの値の代入を使います。ハッシュで表してみましょう。 {“foo” => 1}[:foo] == 1 # true {foo: 1}[“foo”] == 1 # true こんなふうにできたらどんなに楽だったことでしょう。 要するに、以下が欲しいのです。 :foo.hash == “foo”.hash # true Hash(またはSet)で何かを出し入れすると、Rubyは常に入力をObject#hashでいわゆるハッシュ関数として扱います。2つのオブジェクトが等しい場合、両者は同じhashを返すべきです。さもないとRubyがHashからオブジェクトをうまく見つけられなくなります。次の例をご覧ください。 class Something def initialize(val) @val == val end attr_reader :val def ==(another) val == another.val end end a = Something.new(1) b = Something.new(1) hash = {a => “text”} hash[a] # => “text” hash[b] # => nil これはいわゆるValue Objectを定義しています。あるクラスがその1つ以上の属性によって定義され、それを比較に用いています。しかし私たちはhashメソッドをまだ実装していないので、RubyはそれらがHashのキーとしても利用される可能性を認識しません。 a.hash # => 2172544926875462254 b.hash # => 2882203462531734027 2つのオブジェクトが返すhashが同じ場合、両者が等しいとは限りません。利用できるハッシュ値の個数には上限があり、衝突はめったなことでは起きません。しかし2つのオブジェクトが等しい場合は同じhashを返すべきです。 class Something BIG_VALUE = 0b111111000100000010010010110011101011000100010101001100100110000 def hash [@val].hash ^ BIG_VALUE end end 普通ハッシュ値の計算では、すべての属性が正確に一致する配列との衝突を回避するために、すべての属性の配列のhashと巨大な乱数値とのXORを取ります。 言い換えると、欲しいのは以下です。 Something.new(1).hash != 1.hash Something.new(1).hash != [1].hash しかしこれは脱線でした。メリットの話に戻りましょう。 繰り返しますが、私は以下だったらよいのにと思っています。 {“foo” => 1}[:foo] == 1 # true {foo: 1}[“foo”] == 1 # true そのためには以下が必要です。 :foo.hash == “foo”.hash # true しかしここが重要です。現時点ではシンボルのハッシュ値算出は文字列のハッシュ値算出の2〜3倍高速であるようです。私はその理由を知りません。シンボルはイミュータブルですが、おそらくシンボルは事前に算出済みのハッシュ値を持っているか、メモ化されたハッシュ値を持っているのではないでしょうか。ハッシュ値が変わらないのがその根拠ですが、私にはまだよくわかっていません。しかしこれが理由であれば、frozenかつイミュータブルな文字列が遅延算出またはメモ化されたハッシュ値も持てばよいのではないかと想像できます。 世の中には、以下の事実に依存しているライブラリやアプリが多数あると信じています。 :foo.object_id == :foo.object_id 明らかにこの動作は変えるべきではありません。しかし私は、もし仮にRubyのシンボルが文字列であり、かつRuby内部にそれらの一意なリストが保持されるのであれば、上で行ったように何の問題もなく動作すると信じています。 結局、常に同じシンボルを得られるという事実は、Ruby実装のどこかで単に以下の対応付けがなされていることを示しています。 {“foo” => Symbol.new(“foo”)} なお、かつてのシンボルはガベージコレクションすら行われていませんでしたが、現在は行われています。 {“foo” => “foo”.freeze} 仮にRuby内部のどこかで上のようになっているしたら、:fooを求めたときにも同じオブジェクトを得られるでしょう。 :foo.object_id == :foo.object_id # true :foo.equal?(:foo) # true 先を続けましょう。問題があるのはこのあたりです。 foo = “foo” foo.equal?(foo.to_s) # true RubyのString#to_sは基本的にselfを返します。したがって、仮にシンボルがfrozenな文字列だったとしたら、以下は動かないでしょう(実際には動きますが)。 foo = :foo bar = foo.to_s bar … Continue reading Rubyのシンボルをなくせるか考えてみた(翻訳)