[ 目次, 前節, 次節, 索引 ]

3.3.1 可変リスト構造



対の基本演算---cons, carおよびcdr---はリスト構造を構成したり, リスト構造の部分を選択したりに使うことが出来るが, それらはリストを修正することが出来ない. appendlistのような, これまで使ってきたリスト演算についても, それらはcons, carおよびcdrを使って定義出来るので, 同じである. リスト構造を修正するには新しい演算が必要である.



図3.12 リストx: ((a b) c d)y: (e f)



図3.13 図3.12のリストへの(set-car! x y)の効果



図3.14 図3.12のリストへの(define z (cons y (cdr x)))の効果



図3.15 図3.12のリストへの(set-cdr! x y)の効果

   対の基本変更子はset-car!set-cdr!である. set-car!は二つの引数をとり, 第一の引数は対でなければならない. それは, car ポインタをset-car!の第二引数へのポインタで取り替え, 対を修正する. 16

   例として図3.12に示すように, xがリスト((a b) c d), y がリスト(e f)に束縛されているとする. 式(set-car! x y)の評価はxが束縛されている対を, そのcaryの値で取り替えて, 修正する. 演算の結果を図3.13に示す. xの構造は修正され, 今は((e f) c d)と印字されるであろう. 取り替えられたポインタが指していたリスト(a b)を表現していたいくつかの対は, 今は元の構造から切り離されている.17

   図3.13と, xyは図3.12の元のリストに束縛されているとし, (define z (cons y (cdr x)))を実行した結果の図3.14を比べよう. 変数zcons演算で作り出した新しい対に束縛されている; x が束縛されているリストは変らない.

   set-cdr!は演算set-car!に似ている. 違いは対のcarポインタでなく, cdrポインタを取り替える. 図3.12のリストに (set-cdr! x y)を実行した効果を図3.15に示す. ここではxcdrポインタが(e f)へのポインタに取り替っている. 又リスト(c d)xcdrであったが, 今は構造から切り離されている.

   consは新しい対を作り出して新しいリスト構造を作るが, set-car!set-cdr!は現存の対を修正する. 実際consは二つの変更子と, 既存のリスト構造の部分にはない新しい対を返す手続きget-new-pairを使って実装出来る. 新しい対を貰い, そのcarcdrのポインタを, 指定するオブジェクトに設定し, 新しい対をconsの結果として返す.18


(define (cons x y)
  (let ((new (get-new-pair)))
    (set-car! new x)
    (set-cdr! new y)
    new))

問題 3.12


リストを連結する次の手続きは2.2.1節で説明した:
(define (append x y)
  (if (null? x)
      y
      (cons (car x) (append (cdr x) y))))
appendxの要素を次々とyconsして新しいリストを作る. 手続きappend!appendに似ているが, 構成子ではなく変更子である. xの最後の対を, そのcdryになるように修正し, リストを一緒に張り合せて連結する. (空のxについてappend!を呼び出すとエラーになる.)
(define (append! x y)
  (set-cdr! (last-pair x) y)
  x)
last-pairはその引数の最後の対を返す手続きである:

(define (last-pair x)
  (if (null? (cdr x))
      x
      (last-pair (cdr x))))
次の対話を考える.
(define x (list 'a 'b))

(define y (list 'c 'd))

(define z (append x y))

z
(a b c d)

(cdr x)
⟨応答⟩

(define w (append! x y))

w
(a b c d)

(cdr x)
⟨応答⟩
欠けている⟨応答⟩は何か. 箱とポインタの図を描いて説明せよ.

問題 3.13


問題3.12で定義したlast-pair手続きを使う次のmake-cycle手続きを考える:

(define (make-cycle x)
  (set-cdr! (last-pair x) x)
  x)
その時,
(define z (make-cycle (list 'a 'b 'c)))
で作り出す構造zを示す箱とポインタ図を描け. (last-pair z)を計算しようとすると何が起きるか.

問題 3.14


次の手続きは分り難いが極めて有用である.

(define (mystery x)
  (define (loop x y)
    (if (null? x)
        y
        (let ((temp (cdr x)))
          (set-cdr! x y)
          (loop temp x))))
  (loop x '()))
loopは, 次の行のset-cdr!cdrを壊すので, 「一時的」変数tempxcdrの前の値をとっておく. 一般にmysteryが何をするか説明せよ. v(define v (list 'a 'b 'c 'd)) で定義したとする. vが束縛されているリストを表現する箱とポインタ図を描け. (define w (mystery v))を評価するとしよう. この式を評価した後の構造vwを示す箱とポインタ図を描け. vwの値として何が印字されるか.
共有と同一
3.1.3節では代入の導入で持ち上った「同じ」と「変化」の理論的論点を話した. この論点は実際には異るデータオブジェクト間で個々の対が共有された(shared)時に現れる. 例えば
(define x (list 'a 'b))
(define z1 (cons x x))
で作った構造を考えよう. 図3.16に示すようにz1carcdrが同じ対xを指す対である. z1carcdrxを共有するのは, consの実装の直截な方法の結果である. 一般にconsを使ってリストを構成すると, 個々の対が多くの異った構造で共有されるような対の絡み合った構造になる.



図3.16 (cons x x)で作ったリストz1



図3.17 (cons (list 'a 'b) (list 'a 'b))で作ったリストz2

   図3.16に対し, 図3.17は

(define z2 (cons (list 'a 'b) (list 'a 'b)))
で作り出した構造を示す. この構造では, 実際の記号は共有されているが, 二つの(a b)リストにある対は別である.19

   リストとして考えると, z1z2は「同じ」リスト((a b) a b)を表現する. 一般にリストをcons, carおよびcdrだけを使って演算すると, 共有は検出不能である. しかしリスト構造に変更を認めると, 共有は重要になる. 共有の作る違いの例として, それを作用させる構造のcarを修正する次の手続き

(define (set-to-wow! x)
  (set-car! (car x) 'wow)
  x)
を考えよう. z1z2は「同じ」構造だが, それらにset-to-wow!を作用させると, 異る結果になる. z1ではcarを変えるとcdrも変る. z1ではcarcdrも同じ対だからだ. z2ではcarcdrは別なので, set-to-wow!carだけを修正する:
z1
((a b) a b)

(set-to-wow! z1)
((wow b) wow b)

z2
((a b) a b)

(set-to-wow! z2)
((wow b) a b)

   リスト構造での共有を検出する一つの方法は, 2.3.1節で二つの記号が等価かどうかをテストする方法として説明した述語eq?を使うことである. 更に一般には, (eq? x y)xyは同じオブジェクトであるか(つまりxyはポインタとして同じか)をテストする. そこで図3.16と3.17で定義したz1z2では(eq? (car z1) (cdr z1)) は真であり, (eq? (car z2) (cdr z2))は偽である.

   後の節で見るように対により表現出来るデータ構造のレパートリを大いに拡張するのに, 共有が利用出来る. 一方, 構造の修正は, たまたま修正した部分を共有していた他の構造にも影響するので, 危険でもある. 変更演算子set-car!set-cdr!は注意して使うべきである: データオブジェクトがどう共有されているかの十分な理解がないと, 変更は予期せぬ結果をもたらす.20

問題 3.15


上の構造z1z2へのset-to-wow!の効果を説明する箱とポインタ図を描け.

問題 3.16


Ben Bitdiddleはリスト構造中の対の個数を数える手続きを書こうと決心した. 「簡単だ」と考えた. 「任意の構造の対の個数はcarにある個数足すcdrにある個数足す今いる対の1である.」そこでBenは次の手続きを書いた:

(define (count-pairs x)
  (if (not (pair? x))
      0
      (+ (count-pairs (car x))
         (count-pairs (cdr x))
         1)))
この手続きが正しくないことを示せ. 特に三つの対で出来ているリスト構造で, Benの手続きが3を返すもの, 4を返すもの, 7を返すもの, 何も返さないものを表現する箱とポインタ図を描け.

問題 3.17


問題3.16のcount-pairsで, 任意の構造の異る対の個数を返す正しい解を考えよ. (ヒント: どの対が既に数えられたかを覚えておくのに使う補助のデータ構造を補正しながら構造を渡り歩け.)

問題 3.18


リストを調べ, そこに循環が含まれるか(つまりcdrを順にとってリストの終りを見つけようとするプログラムが無限ループに入るか)を決める手続きを書け. 問題3.13はそういうリストを構成した.

問題 3.19


一定量のスペースしか使わないアルゴリズムを使って問題3.18を再試行せよ. (非常に賢明な考え方が必要だ.)
変更は単なる代入
合成データを説明した時, 2.1.3節では, 対は手続きを使っただけでも表現出来るのを見た:

(define (cons x y)
  (define (dispatch m)
    (cond ((eq? m 'car) x)
          ((eq? m 'cdr) y)
          (else (error "Undefined operation -- CONS" m))))
  dispatch)


(define (car z) (z 'car))


(define (cdr z) (z 'cdr))
可変データについても同じことが見られる. 可変データオブジェクトを代入と局所状態を使った手続きとして実装出来る. 例えば, 3.1.1節のmake-accountを使って銀行口座を実装したのと似たように, set-car!set-cdr!が扱えるよう, 上の対の実装が拡張出来る:

(define (cons x y)
  (define (set-x! v) (set! x v))
  (define (set-y! v) (set! y v))
  (define (dispatch m)
    (cond ((eq? m 'car) x)
          ((eq? m 'cdr) y)
          ((eq? m 'set-car!) set-x!)
          ((eq? m 'set-cdr!) set-y!)
          (else (error "Undefined operation -- CONS" m))))
  dispatch)


(define (car z) (z 'car))


(define (cdr z) (z 'cdr))


(define (set-car! z new-value)
  ((z 'set-car!) new-value)
  z)


(define (set-cdr! z new-value)
  ((z 'set-cdr!) new-value)
  z)

   可変データの振舞いを説明するには, 理論的には代入だけが必要だ. われわれの言語にset!を認めると, 代入だけではなく, 一般の可変データの問題も浮び上ってくる.21

問題 3.20


上の対の手続き実装を使って一連の式

(define x (cons 1 2))
(define z (cons x x))
(set-car! (cdr z) 17)

(car x)
17
の評価を示す環境の図を描け. (問題3.11と比べよ.)



16 set-car!set-cdr!は実装に依存した値を返す. set!と同様に, これらは効果のためにだけ使う.

17 このことから, リストの変更演算は, アクセス可能な構造の部分ではない「ごみ」を作り出すことがあるのを知る. 5.3.2節のLispの記憶管理システムには, 不要になった対の使っているスペースを探し再生する ごみ集め(garbage collection)があることを見る.

18 get-new-pairはLispの実装に必要な記憶管理の一部として実装しなければならない演算の一つである. 5.3.1節でこれを議論する.

19 二つの対が別なのはconsは呼出し毎に新しい対を作るからである. 記号は共有する; Schemeではどの名前でも 記号は唯一である. Schemeには記号を変化させる手段はなく, この共有は検出不能である. 共有の故に単にポインタの等価を調べるeq?を使って記号が比べられる.

20 可変データオブジェクトの共有に関する難しさは, 3.1.3節で持ち上った「同一」と「変更」の基盤の論点を思い起す. そこでは言語に変更を認めると合成オブジェクトは, それが作られた部品とは何か違う「同一性」を持たなければならないと述べた. Lispではこの「同一性」はeq?でテスト出来る等価性(つまりポインタの等価性)と考える. 大抵のLispの実装ではポインタは本質的に記憶装置のアドレスだから, オブジェクトの同一性の定義という「問題を解決する」のに, データオブジェクト「自身」は, 計算機の記憶場所の特別な部分に記憶された情報であると規定すればよい. これは単純なLisp システムでは十分だが, 計算モデルの「同じ」ことの論点を解決する一般的方法にはなりえない.

21 他方, 実装の観点から, 代入はそれ自身可変データ構造である環境の修正を必要とする. つまり代入と変更は同能力なもので, 一方は他方を使って実装出来る.




[ 目次, 前節, 次節, 索引 ]