リテラルの意味、の話

Ruby 3.0 における immutable string literal なるものの話が出ているわけですが( http://www.atdot.net/~ko1/diary/201510.html#d5 http://d.hatena.ne.jp/ku-ma-me/20151004/p1 )、その焦点の直接の話題ではないですが、その周辺の話。

いわゆる意味論の方の話がメインになりますが、形式的意味論(formal semantics)の話ではないので、タイトルでは「意味」としました。

データ型とリテラル

コンピュータ科学に詳しくないと思われる筆者が書いたと思われるプログラミング言語の紹介などで、データ型を紹介する、という節なのに、リテラルを紹介しているのを目にしたりします。リテラルは具体的で、プログラムの字面的に、見た目にわかりやすいからだと思いますが、あるプログラミング言語でサポートしているデータ型であっても、そのリテラルは無いという型は多いでしょう。また、データ型をユーザ定義できる言語も多いと思いますが、リテラルをユーザ定義できる言語はそんなに多くなさそうに思います(Lisperから「それリードマクロで……」という声が)。

一般に、データ型とは「同様に扱うことができるような値の集合」というような抽象的なもので、一方、リテラルは「ソースコード上の文字通りの値という意味を持つ式」というような具体的なもの、と言えます。

(大昔の言語などでは、PRINT文にだけ文字列が書けるといったような「リテラルはあるが、その型は無い」というようなこともあったと思われるが、今ではそういったことは普通のプログラミング言語ではまず無いと考えていいだろう)

定数とリテラル

これは私も以前はだいぶ混乱していて、今でも某所で演習用に使われているらしい言語処理系内部の変数名や終端記号名などはかなり怪しいはずですが、英語で、定数はconstant、リテラルはliteralであるように、明確に別のものです。たとえば、架空の言語での例になりますが、

const PI = 3.14159265358979323846

のようにあるとすると、この定義の左辺側の「PI」が定数で、右辺側の「3.14159265358979323846」がリテラルなわけです。定数に設定するものとしてリテラルしか書けない、ということにすると言語が単純になるのでそういった言語も多いでしょうが、混同するのは良くありません(というか、混同してはいけません)。

ミュータブルとイミュータブル

Rubyは、もしかしたらちょっと珍しい方なのかもしれませんが、デフォルトの文字列型(文字列オブジェクト)がミュータブルです(たとえば、Javaでは普通の文字列はイミュータブルなStringクラスで、「ミュータブルな文字列」のようなものにはStringBufferクラスやStringBuilderクラスを使います)。

そして今回、Ruby 3.0 で「変える!」(かも?)という話になっているのは、Stringクラス(のインスタンス)全て、という話ではなく、文字列リテラル(が作るオブジェクト)が、作られるのが終わると同時にfreezeされたものになる、という話であるのに注意してください。

リテラルの意味、について

やっと本題という感じですが、同じようなリテラルに見えても、実は実際の働きは異なることがあります。

irbで次のようなRubyコードを試してみます。

irb> def regexp1; return /foo/; end
=> :regexp1
irb> regexp1().__id__
=> 17207672380
irb> regexp1().__id__
=> 17207672380

__id__ の値が全く同じですので、同じRegExpオブジェクトが返されていることがわかります(これはRegExpオブジェクトがイミュータブルだからできることでもあります)。

これを少し変えて、パターン中に引数を埋め込んだりすると……

irb> def regexp2(x); return /foo#{x}bar/; end
=> :regexp2
irb> regexp2(1).__id__
=> 17202212760
irb> regexp2(2).__id__
=> 17207282320

このように、違うオブジェクトが返るようになります。というかそうでないと変ですよね。

リテラルではありませんが、Lispで、クウォートされたリストとlist関数によるリストの違い、というのがこれにちょっと似ています。ここではSchemeGauche)を使いますが、

gosh> (define (func-foo) '(1 2 3))
func-foo
gosh> (define (func-bar) (list 1 2 3))
func-bar
gosh> (eq? (func-foo) (func-foo))
#t
gosh> (eq? (func-bar) (func-bar))
#f

クウォートされたリストのほうは別な呼出しに対して同じリストが、list関数によるリストでは違うリストが返っています。

これを破壊的に操作すると、次のように謎なことになりますが、

gosh> (let ((a (func-foo))) (set-car! a 5))
#<undef>
gosh> (func-foo)
(5 2 3)

このようなことをするのはSchemeの言語仕様ではエラーとされています(R5RSでは§3.4)。

(追加)そして、Rubyの文字列リテラルがfrozenに、という話

前述のようにRubyのStringオブジェクトはミュータブルですから、Rubyの文字列リテラルは「いつも同じオブジェクトが返る式」ではなく「毎回、その表現に相当する新しいオブジェクトが作られる式」です(でした)。そして、今回話題になっているのは、Stringオブジェクト全般ではなく、文字列リテラルについて、それで作られたオブジェクトが、作られた直後にfrozenされてしまうようにする、という変更を入れるかどうするか、入れるとしてどのようにするか(とにかく変えてしまうか、マジックコメントを必要とするか、等)ということになります。