シンボルとは何か その1(前編) - 文字列の同一性と同値性

シリーズ・RubyのSymbol

とりあえず分かりやすいものから見る、ということで、「intern化された文字列」としてのシンボルを説明する。そのためにはRubyにおける文字列値について詳しく考える必要がある。

同一性と同値性

次は真だろうか。

"abc" == "abc"

勿論、真である。Rubyでは String#== はオーバーライドされていて、長さと内容 *1 が等しい文字列同士を比較すると真になる。

さて、次は真だろうか。

"abc".equal? "abc"

これはたぶん偽だろう *2 。それは何故か。 Object#equal? は引数のオブジェクトがレシーバ自身である、まったく同一のオブジェクトを指すときのみ、真となる。

equal? のようにオブジェクトとして等しいことを「同一性」と言う。これに対して String#== のように示す内容が等しいことを「同値性」と言う。

Javaプログラマはこう考えると分かりやすいだろう。同一性とはJavaにおけるこれである。

str == "abc"

同値性とはJavaにおけるこれである。

str.equals("abc")

表記がRubyとほぼ逆なのがややこしいけど、まあ、文字列比較を == でやると死ねるというのはJavaプログラマは慣れているだろう。

同一ならば必ず同値でもある。自分自身とは同値、ということだ。当たり前である*3

けれども、同値でも同一とは限らない。同じ文字列を表すStringオブジェクトでも、別のオブジェクトかも知れない。だからJavaでは面倒でも文字列比較には String#equals を使うのだ。

ま、分かってる人にはつまんない話だったね。

文字列リテラルの指すもの

さて、では次は真を返すか。

buf = []
2.times do
  buf << "abc"
end

puts buf[0].equal?(buf[1])

真と答えた人は残念。たぶん偽である*4Javaだったら、たぶん真なのだが。

String[] buf = new String[2];
for (int i = 0; i < 2; ++i) {
    buf[i] = "abc";
}

System.out.println(buf[0] == buf[1]);

同一箇所の同一のリテラルを代入したのに、Rubyでは何故違うオブジェクトなのか。Rubyは"abc"というリテラルを評価する度に、毎回新しい文字列オブジェクトを生成して返すからだ。どうしてそんな無駄なことをするのか。

実際Javaだと、この文字列リテラルはコンスタントプールの中にある唯一の文字列を差し続ける。毎回、同じindexで ldc やなんかをするのね。

Rubyがこうやっている理由は、文字列の式展開が可能で、しかも文字列はmutableだからだ。だって、プログラムがこうだったらどうする?

buf = []
2.times do
  buf << "abc"
end

buf[0][0] = ?z
puts buf[0], buf[1]

JavaではStringはimmutableだ。一度生成された文字列オブジェクトの値は変えられない。他の値が欲しければ別の文字列オブジェクトを作るしかない。だから、コンパイラは最適化のため、同一内容の文字列ならできるだけ同一のオブジェクトを使い回すようにできる。その辺にある普通のjavacは当然そうする。

しかしRubyでは文字列オブジェクトの値は変更できる。だから、今は同一内容でもあとでそうでなくなるかも知れない。故に、リテラルといえども、評価する度に新しいオブジェクトを生成しないとおかしくなる。

まして、式展開なんか含んでいたら。

buf = []
2.times do
  buf << "abc #{Time.now}"
  sleep 1
end

ま、 "abc" == "abc" ぐらいだったら文字列を書き換えていないことは明らかなので、処理系が気を利かせてJavaみたいな最適化をしても良いのではあるけど。でも、プログラム全体にわたって本当に書き換わることがないと断言するのは、実用上は困難だ。それはすごく難しいコンピュータサイエンスの問題だと思うから、ささださんとか処理系研究者の人に期待。

余談

実はCでもRubyと似たような問題は発生する。Cでは文字列はmutableだからだ。

#include <stdio.h>

int main(void)
{
    char *buf[2];
    int i;

    for (i = 0; i < 2; ++i)
    {
        buf[i] = "abc";
    }
    buf[0][0] = 'z';
    printf("%s, %s", buf[0], buf[1]);
    return 0;
}

でもCはRubyのように親切ではない。こういうことは、やる奴が悪いということになっている。結果は「 未定義 」 である。

古き良き時代の環境だったら、たぶん"zbc, zbc"と出たんだろうけどね。今どきのメモリ保護の効いた環境で動かすと異常終了するケースが多いんじゃなかろうか。ま、これがコンパイル通るあたりは「自分の足を撃ち抜く自由」というやつか。

予告

シンボルとは何か その1(後編) - 文字列のintern 」に続く

*1:と、Ruby 1.9以降ではエンコーディング

*2:私はこれは「実装依存」だと思うけど、何しろRubyにはそのところの言語仕様が明文化されていないのでよく分からない。2007年9月15日現在、最新版のCRubyとJRubyでは偽だった

*3:その辺につっこんで行くのが数学屋の習性だけど

*4:私はこれも「実装依存」であるべきだと思うけど