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';
じゃあ変更できるオブジェクトとは?
原則として、「プログラム中のリテラル」以外のものは全て変更可能 だと思って良いでしょう。
- immutableオブジェクトの部分オブジェクトはやはりimmutableですので、 例えば (cdr '(a b c)) の戻り値 (b c) はimmutableとみなすべきです。
- immutableオブジェクトを返すと明示している手続きがあります。 例えば symbol->string の戻り値はimmutableです。 symbol->stringの戻り値が変更可能であったら、シンボルの名前が 意図せず変更されてしまうことを防ぐために、symbol->stringは呼ばれる度に 名前の文字列をコピーしなければならなかったでしょう。
Gaucheでの実装は?
ネイティブオブジェクトコードを生成するコンパイラなら、 immutableオブジェクトをプログラムテキスト領域に置くことで、 ほとんど追加コスト無しにimmutableオブジェクトへの変更を 捕まえることができます。
インタプリタであっても、メモリアロケーションを処理系が かりかりコントロールしているものであれば、例えばimmutable objectは専用のページからアロケートするようにしておけば、 オブジェクトの上位数ビットだけを見てimmutableかどうかの 判断をすることができるでしょう。
あいにくGaucheではメモリアロケーションはBoehm GCに丸投げして しまっているので、そういう対策は取れず、オブジェクト毎に フラグを持つしかなくなります。文字列に関しては symbol->string等、どうしてもimmutableなオブジェクトを返したい ものがあったので、immutableフラグを押し込みました。 ベクタに同様なフラグを付けるのも難しくないでしょう。 しかしコンスセルに関しては、フラグを押し込む場所が無く、 そのために1ワード増やすのも嫌なので困りものです。 (やはり、GCが自前ならポインタの余りビットを使うことも できるんですが)。
実装の戦略
- quoted list もリテラルとみなして変更不可とする処理系の場合って set-car! とか set-cdr! なんかに対して 変更不可ってことで書き換えできないってことですね? すごい違和感を感じてしまうんですけど、私のスタイルがおかしいのかな。 ちょっとお試しでリストをいじるようなコードを書くときには、 quoted list で書かないで、 list で作るようにするのが普通って事なんでしょうか。cut-sea:2003/06/12 22:40:19 PDT
- そうです。C言語のリテラル文字列と対比すればそんなに違和感は無いのでは? 参考:http://www-2.cs.cmu.edu/Groups/AI/html/faqs/lang/lisp/part3/faq-doc-14.html (ここの例はCommon Lispなので、remove/deleteの意味が違うことに注意!)
なるほど、いじりたきゃ 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メモリに置くことは可能です。 リテラルを置くメモリページというのを持っておいて、式のコンパイル時に だけそのページを書き込み可能にして、実行時にはアクセスしたら例外が 発生するようなフラグをたてておけば良い。
- 逆に評価時に quote が噛んだら immutable にするってのは元々の目的から見るとあまり正しくないような気がしてきました。 scheme48 のこの動作って本来目的とするリテラルを守るってのとは違うのでは? 例えば自力で関数を作り出すようなプログラムを作った時、(せいぜいランダムに組み合わせてであってもいいけど) quote が入ったためにエラー発生とかってなんか違う気がしますね。cut-sea:2003/06/15 19:23:02 PDT
Shiro: これは、evalに渡したS式が評価後にimmutableになってしまうことに ついて、ですよね。確かにちょっと変ですね。 evalに渡すのは単なるデータであって、しかも副作用は期待していない わけですから、渡したデータそのものの属性がevalの前後で変わってしまうのは おかしいです。
- そういう意味です。分かり難くて申し訳ありません。cut-sea
議論、コメント
- ちなみに C の例であげてる文字列リテラル変更の禁止は ANSI-C
以降の規定です.だから古いプログラムでは結構書き換えてるのが
あったりします.gcc に -fwritable-string なんていうオプション
があるのもそのせい.
- mktemp() に定数文字列を渡してたり
- regexp->string regexp 正規表現regexpを記述する元になった文字列を返します。返される文字列は変更不可な文字列です. と、あるので
gosh> (let ((org (regexp->string #/abc/))) (string-set! org 0 #\X) org) "Xbc"
あれ、変更できちゃった。文句を言われるのが正しい挙動? sakae- あ、バグ。Shiro
- 戯 そういやBoehmって、memory manager(というかmanageされる領域の塊)を、識別可能なかたちで複数持つ(Objectみたいに)ことが出来ないわけですか。ですね。がくっ…
- Shiro: いや、できます。あまり手軽ではないですが。新しいobjkindを定義して、 適切なハンドラを書いてやれば。で、ポインタを手にしたらそれがどのobjkindに属するかを queryすることもできます。ただ、ハンドラ内で何でもできるわけじゃないし、ポータビリティを 考えるとあまりメモリレイアウトを好き勝手にいじるわけにもいかないので… (あ、でも、コンスセルだけ別のobjkindっていうのはありかも。)