Ruby でもっと文字列を使い回したい


きっかけ

Python3 で FizzBuzz を書くときに、次のようにすると、

def fizzbuzz(n):

f = 'Fizz' if n % 3 == 0 else ''
b = 'Buzz' if n % 5 == 0 else ''
fb = f + b
return fb or n

fizzbuzz(3) is fizzbuzz(6)        => True

fizzbuzz(5) is fizzbuzz(10) => True
fizzbuzz(15) is fizzbuzz(30) => True

となり、'Fizz' も 'Buzz' も 'FizzBuzz' も同じ文字列オブジェクトを使い回しています。Python の文字列が immutable だから、安全に使い回せるんですね。

これを Ruby でもやってみたいということです。

RubyInstaller の 2.5.1 を使います。


つまずき

Ruby では、現状、文字列が使い回せるのは 文字列のリテラル("literal") に、-"literal""literal".freeze したときだとされています。+ メソッドで文字列を結合すると、メソッド呼び出しのたびに新たな文字列オブジェクトが作られるため、Python のようにはいきません。

def fizzbuzz(n)

f = (n % 3).zero? ? -"Fizz" : -""
b = (n % 5).zero? ? -"Buzz" : -""
fb = f + b
fb.empty? ? n : fb
end

fizzbuzz(3).equal? fizzbuzz(6) => false
fizzbuzz(5).equal? fizzbuzz(10) => false
fizzbuzz(15).equal? fizzbuzz(30) => false


ひらめき

Ruby のマニュアルには、式展開はリテラルだと書かれています。だとすると、式展開と -@ なり freeze なりを組み合わせれば、使い回しのきく文字列が合成できるのではないか?

def fizzbuzz(n)

f = (n % 3).zero? ? -"Fizz" : -""
b = (n % 5).zero? ? -"Buzz" : -""
fb = -"#{f}#{b}"
fb.empty? ? n : fb
end

fizzbuzz(3).equal? fizzbuzz(6) => true
fizzbuzz(5).equal? fizzbuzz(10) => true
fizzbuzz(15).equal? fizzbuzz(30) => true

大成功です。こんなこと当たり前で、ただ私が不勉強なだけなのかもしれないのですが、まだ知らない人とこの感動を分かち合いたいと思い、勇気を持って投稿しました。


いろいろ実験していて、「ひょっとしてバグ?」と思った点



  • fb = -"#{f}#{b}"fb = "#{f}#{b}".freeze にすると、文字列が使い回されない。

OMG! なぜ、両者で振る舞いを変えるのでしょう?



  • # frozen_string_literal: true にすると、文字列が使い回されない。

ますます、OMG! です。初めから文字列リテラルが frozen なんだから、- が邪魔なのかと思って削ってみたものの変化はありませんでした。

この振る舞いは意図的なものなのでしょうか?


結論的なもの

# frozen_string_literal: false + -"literal" + 式展開 = 最強