Gauche:ImmutableObject

Gauche:ImmutableObject

sakaeさんとこより。

(string-set! "abc" 0 #\X) ==> Error?

Gaucheでは、

  (string-set! "abc" 0 #\X)  ;; "Xbc" を期待

とするとエラーになる。エラーにならない処理系もあるのに。Gaucheのソースツリーの テストコードを見てみると、

  (string-set! (string-copy "abc") 0 #\X) ==> "Xbc"

とかやっている。何でコピーが必要なんだろう?

R5RSでは、プログラムコード中のリテラル式の値は変更不可(immutable)であると 規定されています (3.4節、4.1.2節)。リテラル式とは、プログラム中に直接 書かれている定数式で、数値や文字列、クオートされた式等です。

この仕様の理由は、リテラルで書かれたオブジェクトはプログラムテキストの 一部とみなされるからです。 コンパイルする処理系ではそういうオブジェクトをメモリの 変更不可領域に配置するかもしれません。インタプリタであっても、リテラル オブジェクトは一個しか作られずにプログラム中で使い回される可能性が ありますから、その唯一のオブジェクトを破壊してしまったら プログラムは意図した動作をしないかもしれません。

なお、R5RSは「エラー」という状態に関して規定していないので、 R5RS準拠の処理系であっても、リテラルオブジェクトを変更した場合に 必ずエラーが報告されるとは限りません。黙って変更を許してしまう 場合もあり、それは処理系独自の拡張とみなされます。

Gaucheでは今のところ、例えばクオートされたリストやベクタは 変更できてしまったりします。 SCMでは リテラル文字列も変更可能なようです。 逆に、stklosはリテラルリストの 変更も厳しくチェックします。

プログラマはたとえ処理系がそれを許していたとしても、 リテラルオブジェクトを変更すべきではないでしょう。

実はこれはScheme特有の問題ではありません。 C言語でもこんなことはやっちゃまずいですね:

  char *a = "abc";
  a[0] = 'X';

じゃあ変更できるオブジェクトとは?

原則として、「プログラム中のリテラル」以外のものは全て変更可能 だと思って良いでしょう。

Gaucheでの実装は?

ネイティブオブジェクトコードを生成するコンパイラなら、 immutableオブジェクトをプログラムテキスト領域に置くことで、 ほとんど追加コスト無しにimmutableオブジェクトへの変更を 捕まえることができます。

インタプリタであっても、メモリアロケーションを処理系が かりかりコントロールしているものであれば、例えばimmutable objectは専用のページからアロケートするようにしておけば、 オブジェクトの上位数ビットだけを見てimmutableかどうかの 判断をすることができるでしょう。

あいにくGaucheではメモリアロケーションはBoehm GCに丸投げして しまっているので、そういう対策は取れず、オブジェクト毎に フラグを持つしかなくなります。文字列に関しては symbol->string等、どうしてもimmutableなオブジェクトを返したい ものがあったので、immutableフラグを押し込みました。 ベクタに同様なフラグを付けるのも難しくないでしょう。 しかしコンスセルに関しては、フラグを押し込む場所が無く、 そのために1ワード増やすのも嫌なので困りものです。 (やはり、GCが自前ならポインタの余りビットを使うことも できるんですが)。

実装の戦略

なるほど、いじりたきゃ lisp の世界(の評価)で新たに作り出せってことですね。 でも私の英語力がへぼいからなのか、最初の方文章で quote はリストを作らないで そのまま返すんだよってありますね。 これ、確かに実装する時には単純に考えると、そうなるのが自然ですよね。 lispリーダが読みこみながらlisp界で使えるオブジェクトのリストを作り上げて行って ルートのオブジェクトをそのまま返して評価してもらう。 quote は結局引数のオブジェクトへのポインタをまんま返すと。 今現在の手元には scheme48 と stk しかないんですけど scheme48 は quoted list は immutable な扱いにするんです。 ちなみにリテラルかどうかってどこで判別するんでしょう。 結局文脈になる?それで判別つく? lispリーダとかが読みながら判断してメモリを割り付ける時に そういう領域に割りつけるのって重そうな気がするんですよ。 そこで試してみる。

> (define z '(1 2 3))
> z
'(1 2 3)
> (set-cdr! z 4)
Error: exception
       (set-cdr! '(1 2 3) 4)

これはいい。 non-quoted なリストの場合はset-cdr! も set-car! も問題無いです。もちろん。 ただ、自己評価式の前に不要に quote が付くのがうっとおしいんですけど、これは別の話。(scheme48/windows98)

> (define x (list 'quote (list 1 2 3)))
> x
''(1 2 3)
> x
''(1 2 3)
> (cdr x)
'((1 2 3))
> (eval x (null-environment 5))
'(1 2 3)
> (set-car! (eval x (null-environment 5)) 4)

Error: exception
       (set-car! '(1 2 3) 4)
1>,reset

Top Level
> (set-car! x 4)
> x
'(4 (1 2 3))
> 

この動きを見るとリーダがやってるんではなくて quote が評価するときにフラグセットしてそうですよね。 では文字列リテラルは??? やっぱ個々の関数が対応している?? まぁその方が納得はいくんですけど、メモリ割当の時にread-only なエリアを 割り当てるっていう実装はどんなこと考えればできるんだと思ったんです。cut-sea

失敬!これじゃ意味が無いや。全然比較になってない。

> (define x (list 'quote (list 1 2 3)))
> x
''(1 2 3)
> (cdr x)
'((1 2 3))
> (cddr x)
'()
> (cdadr x)
'(2 3)
> (set-car! (cdadr x) 4)
> x
''(1 4 3)
> 

これです。 (quote (1 2 3)) の評価を終えると変更不可で、ここの様にリストとして扱うと変更可である。 read-only なメモリにリテラルを割り当てる処理系ではさすがにこんな動作はしないと思われる。cut-sea

Shiro: 「リーダーがどうこうする」というのはちょっと違うかな。 式の評価を「コンパイル」と「実行」に分けて考えれば、コンパイルの段階で クォートされたリストは分かるので、それを特殊な領域に配置することが できるでしょう。evalは内部でコンパイルと実行を同時に行っていると 考えます。

対話時やevalであっても、読んだ式をread-onlyメモリに置くことは可能です。 リテラルを置くメモリページというのを持っておいて、式のコンパイル時に だけそのページを書き込み可能にして、実行時にはアクセスしたら例外が 発生するようなフラグをたてておけば良い。

Shiro: これは、evalに渡したS式が評価後にimmutableになってしまうことに ついて、ですよね。確かにちょっと変ですね。 evalに渡すのは単なるデータであって、しかも副作用は期待していない わけですから、渡したデータそのものの属性がevalの前後で変わってしまうのは おかしいです。

議論、コメント

More ...