ファイヤープロジェクト
リストの実体
2003-10-08T21:00+09:00   matsu
二つのオブジェクトへのポインタがあるとき,そのポインタが指す先は同じ実体なのか,同じ値の異なる実体なのか,といった疑問を解決してみたい.
ここまでの記事では,Lispにおけるオブジェクトの生成に関する詳しい記述や考察はない.だが,何をしたらオブジェクトが生成されるのか,オブジェクトの破棄はどうするのかといったことは,パフォーマンス,メモリ管理の面で重要である.ここではリストの実装であるコンスの実体が何をすれば作られるのか,それを調べる関数は何かといったことについて記述する.さらにオブジェクトの破棄についても記述する.
consはコンスを生成する関数である.
(cons car部が指す先 cdr部が指す先)
二つの引数は順に生成さらたコンスのcar部が指す先,cdr部が指す先である.コンスを生成するのだから,二つの引数が同じ実体か同かを調べる関数eqlを使用すると以下のようになる.
> (setf cons1 (cons 'a nil))
(A)
> (setf cons2 (cons 'a nil))
(A)
> (eql cons1 cons2)
NIL
これはcons1が指すコンスとcons2が指すコンスが別の実体であることを示している.続けて二つの引数が同じ値か同かを調べる関数equalを使用すると以下のようになる.
> (equal cons1 cons2)
T
これはcons1が指すコンスの値とcons2が指すコンスの値が等しいことを示している.ついでに
> (eql (car cons1) (car cons2))
T
からcons1のcar部とcons2のcar部の実体が同じであることを踏まえると,cons1,cons2の実体と参照関係は以下のようになっている.
Lispにはポインタがある.これまでの記事のサンプルでもポインタを扱ってきた.それはコンスのcar部,cdr部がオブジェクトへのポインタであったし,変数もオブジェクトへのポインタであった.ただしLispにおいてはポインタを明示的に操作する必要がない.実際に今まで示して来たサンプルの多くは明示的ではないが,結果としてポインタ操作をするものであった.しかし,ポインタを明示的に操作する必要がないというのは,ある時は便利だが理解していないと問題が生じることがある.
> (setf ptr1 '(a b c))
(A B C)
> (setf ptr2 ptr1)
(A B C)
> (eql ptr1 ptr2)
T
上の例ではeqlによってptr1とptr2が同じオブジェクトを指していることを確認している.すなわち変数ptr1,ptr2はポインタであり,オブジェクト(a b c)を指している.
ptr1とptr2はポインタであるから,上の例から続けて指しているオブジェクトを変更すると,当然以下のようになる.
> (setf (car ptr1) (car '(d)))
D
> ptr1
(D B C)
> ptr2
(D B C)
この状態を図示する.
前節ではポインタptr1をポインタptr2にコピーした.
> (setf ptr2 ptr1)
先に示したとおり,これはポインタのコピーであり,ポインタの指すオブジェクトまではコピーしていない.したがって,ptr1からたどったオブジェクトを変更すると,ptr2からたどったオブジェクト(これはptr1からたどったオブジェクトと同じである)も変更される.これを浅いコピーという.Lispでは深いコピー,すなわちポインタの指すオブジェクトまでをもコピーする関数がある.
(copy-list リスト)
この式の評価値は引数のリストのコピーである.以下の例ではptr1の指すオブジェクトのコピーをcopy-listによって作成しptr2でポイントしている.
> (setf ptr1 '(a b c))
(A B C)
> (setf ptr2 (copy-list ptr1))
(A B C)
> (eql ptr1 ptr2)
NIL
> (equal ptr1 ptr2)
T
eqlにより,ptr1とptr2は別のオブジェクトを指すが,equalによりptr1が指すオブジェクトとptr2が指すオブジェクトの値が同じであることを示している.Lispの関数呼出の多くは副作用を伴わない関数である.これはLispの基本ポリシーである(※).例えば任意の数のリストを連結するappend.
(append リスト リスト リスト...)
を使用してみる.
> (setf lst1 '(a b))
(A B)
> (setf lst2 (append lst1 '(c) '(d)))
(A B C D)
> lst1
(A B)
> lst2
(A B C D)
このように,lst1のリストは変更されていないが(lst1の最後のコンスのcdr部の指す先はnilのまま),lst2はリスト(a b),(c),(d)を連結したものとなっている(lst2の二つめのコンスのcdr部は三つめのコンスを指している).
※ パフォーマンスの関係でこの限りでないものもある.副作用によって,ポインタである引数の参照するオブジェクトを変更してしまう関数を「破壊的な関数」という.破壊されては困るときに破壊的な関数を使用する時は引数にコピーを渡す.例:sort.
リストの要素をさらにリストとしていくと,ツリーを構成できる.
(setf atree '(((a) (b)) (c (d))))
先述の深いコピーの概念から,
(setf btree atree)
では,ツリーへの参照がコピーされるだけで,ツリー全体のコピーが作成されるわけではない.深いコピーを作成する関数として,copy-treeがある.
;; copy-treeの場合
> (setf btree (copy-tree atree ))
(((A) (B)) (C (D)))
;; btreeの(cdr (car btree))をnilに変更
> (setf (cdr (car btree)) nil)
NIL
;; atreeの(cdr (car btree))は変更されていない
> (cdr (car atree ))
((B))
;; btreeの(cdr (car btree))は確かに変更されている
> (cdr (car btree))
NIL

;; copy-listの場合
> (setf ctree (copy-list atree ))
(((A) (B)) (C (D)))
;; ctreeの(cdr (car btree))をnilに変更
> (setf (cdr (car ctree)) nil)
NIL
;; atreeの(cdr (car btree))も変更されている
> (cdr (car atree ))
NIL
;; もちろんctreeの(cdr (car btree))は変更されている
> (cdr (car ctree ))
NIL
このように,copy-listでは引数としたリストの(再帰的に)cdr部のみをコピーするので,car部の参照先のコンス
(cdr (car ctree))
はコピー元のatreeのものと同じ実体である.一方,copy-treeでは引数としたリストのcar部も(再帰的に)コピーするので,
(cdr (car btree))
はコピー元のatreeのものとは実体が異なる.Lispにはcopy-listとcopy-treeのように,似たような機能でもcar部のみを再帰的に処理する関数と,car部とcdr部を再帰的に処理する関数がある.後者はツリー操作関数と言われたりするが,要点は「対象がツリー」と考えるよりも「対象がcar部とcdr部」と考える方が適当である.
matsu(C)
Since 2002
Mail to matsu