Schemeの“ポインタ”のようなもの、或いは『Scheme入門』をGaucheで読む(1.5)


set-car!/set-cdr! で思ったように行かなかったので色々試行錯誤した結果、分かったことを記しておきます。

環境はEmacsでGauche。

(set-car! '(2 2 3) 1)

(1 2 3)

と評価されるだろうな、と思っていたら

gosh> #<undef>

となって、どうすればいいんだろ? と試行錯誤したところ

(define x '(2 2 3))

とリストを定義してからから

(set-car! x 1)

とするとやはり
gosh> #<undef>
と評価されますが、x自体を評価しますと

(1 2 3)

と評価されます。
これはおそらく、set-car!/set-cdr! が単にリストの 先頭/その残り を入れ替えるのではなく、(この場合)xの指すコンスセルの car/cdr 部へ代入 なのだろうと思われます。
こんな感じなんじゃないでしょうか?

この、「xの指すコンスセルの car/cdr 部へ代入」てさー……

所謂、ポインタだよね。C言語でいうところの。
……うん、知ってた。ポインタの無いプログラム言語なんて無いって、知ってた。

ともかく、Schemeにおける set-car!/set-cdr! がポインタ(矢印)なのだろうと言うことがよく分かる例に、queue を挙げます。参考例は
「Scheme 入門10. 代入」

4.1. 待ち行列 (Queue) の実装
より。

ところでこのページののっけから「代入(むやみに)使うべからず」のように注意されてますし、最後にも「関数型で代入はあんまし使わない」というようなことが書かれてますよね。
さらに上述した例、

(define x '(2 2 3))

(set-car! x 1)

のような用法に至っては「プログラミングGauche」(Kahuaプロジェクト (著), 川合 史朗 (監修, 監修) 2008年 オライリージャパン)P121のコラムで
「本当はやってはいけない」
とまで書かれてます。
いやはや、関数型におけるset!一族のこの嫌われようと言ったら。一体何やらかしたんだ、set!一族。関数型言語業界のお偉いさんの娘にでも手を出したのか。

そんなset!系ですが、取り敢えず教科書に載ってる以上使ってみないわけにもいかず、使う以上、使い方も知らないわけにいかんのです。きっと若気の至りだったんです、許してあげてください、関数型言語業界のお偉いさん。

さて「4.1. 待ち行列 (Queue) の実装」のうち、make-queue と enqueue! だけGaucheでやってみました。コードは紹介した
「Scheme 入門10. 代入」
で見るなりコピペするなりしてください。

それでは括弧打ち開始。

(define q (make-queue))

で q というqueueを作ります。この時点で q を評価すると

gosh> (())

となります。今の q はこんな感じですな。

それから

(enqueue! q 'a)
(enqueue! q 'b)
(enqueue! q 'c)

とやっていきますと評価するつど
gosh> (a)
gosh> (a b)
gosh> (a b c)
と評価されます。
ところがq 自体を評価しますと
gosh> ((a b . #0=(c)) . #0#)

……何だこの謎のリストは。

うーん、と悩みに悩み(本当はそこまで悩まず)ググったところ、いらっしゃるものですね、同じような箇所で嵌った方が。はい、こちらです。公式(?(と言うか作者の川合史朗氏のサイト))へのリンク
も貼ってあります。ありがたや。
#0=(c)
は、(c)に #0 というラベルをつけて、同じオブジェクトを #0# で表しているようです。つまり
((a b . #0=(c)) . #0#)

((a b . (c)) . (c))
つまり
((a b c) c)
で、 enqueue!関数 の最後で queue を car しているので ((a b c) c) の car である(a b c)が返されるんですな。
試しに

(enqueue! q 'a)

の時点で q を評価すると
gosh> (#0=(a) . #0#)
と返ってきます。つまり
((a) . (a)) すなわち ((a) a) です。この状態をコンスセルで表すと

このように、同一の (a) が q の car部 cdr部にポイントされていて、それらがそれぞれの (a) では無いことを示すために
gosh> (#0=(a) . #0#)
と記されるようです。
その後順次

(enqueue! q 'b)
(enqueue! q 'c)

とやっていくと

そーれから♪

と、 q の cdr部をポイントするコンスセルが次々と最後のコンスセル(この場合は (a) から (b) 、(b) から (c) )に変わっていき、そしてそれらのコンスセルは q のcar部にポインタされた最初のコンスセル(この場合は (a) )から次々につなげられて(ポインタされて)いくので、q をcar すると (a b c) が返ってくるわけですね……うーん、よくもまあ、こんなアクロバティックなコード、というか技法を思いつくものだなぁ。

なので q 自体を評価すると car部に (a b c) が、 cdr部に (c) がポイントされていて、そしてこの (c) はcar部の (a b c) の最後のコンスセルである (c) と同一であることを示すため、単に
((a b c) c) {もしくは((a b . (c)) . (c))}
ではなく
((a b . #0=(c)) . #0#)
と記されるようです。(ちょっとこんがらがってきた)

では湯浅太一著『Sheme入門』(岩波書店 1991年)の96ページ、巡回リストをやってみましょう。

(define x '(eat rice))

(set-cdr! x x)

さて x を評価しますと……

gosh> #0=(eat . #0#)

と返ってきました。これはコンスセル x の cdr部にコンスセル x 自体が入っている、ということですね。リストにすると

(eat . (eat . (eat . (eat . ……(事象の地平の彼方で) (eat . (eat))))……(事象の地平の彼方で)))

で、結果
(eat eat eat eat eat eat ……(事象の地平の彼方で) eat)
となります。
同書には「こういうことが起こるから強制終了の仕方を調べてやるように」ということが書かれていますので私は
「Gauche暴走開始! Emacsごと強制終了させます! カウントよろしく!」
「3! 2! 1!」
「Emacs可動停止! Gaucheも活動を止めました!」
とやる気まんまんだったので、ちょっと残念でした。