popはマクロで、渡した place を直接書き換えるため、ローカル変数に取り出してからpopしても元のリストには届かないloop for inやloop acrossは要素をローカル変数に束縛するので、popしても元の構造は変わらないloop for onでコンスセルを受け取り(car rest)を渡すか、添字で(aref piles idx)を渡すと、スロットを直接書き換えられる- シーケンスを破壊的に変更したいなら、要素を変数に取り出さず、スロットにアクセスできる place の形のまま
popに渡す必要がある
1. 【失敗例】list を loop for in で走査する
山札 piles の中から特定のカードを引き取る関数を作りました。
しかし、うまくいきません。
(defun take-cards-in-piles (piles card)
(loop for pile in piles
for pos = (position card pile)
when pos
return (loop repeat (1+ pos)
collect (pop pile))))Code language: Lisp (lisp)
popを使って抜き出すカードは返り値に得られたのに、pilesの方からは減っていないのです。
(defparameter *piles* (list (list 1) (list 2) (list 3) (list 4) (list 5)))
(take-cards-in-piles *piles* 5)
;; => (5)
*piles*
;; => ((1) (2) (3) (4) (5)) ← 変わっていないCode language: Lisp (lisp)
これは、loop for pile in piles に問題があります。piles の各 car を pile というローカル変数に束縛して走査します。(pop pile) は pile のバインディングを書き換えるだけで、piles のコンスセルの car スロットには届いていないのです。
*piles* -> [* | *] -> ...
|
v
[5 | nil] ← car スロットは変わらない
^
pile(ローカル変数)が書き換わっても届かない
1.1. 【失敗例】vector を loop across で走査する(届かない)
同様のことは、山札をベクターで持つ場合にも起こります。
(defparameter *piles-vec* (vector (list 1) (list 2) (list 3) (list 4) (list 5)))
(defun take-cards-in-piles-vec (piles card)
(loop for pile across piles
for pos = (position card pile)
when pos
return (loop repeat (1+ pos)
collect (pop pile))))
(take-cards-in-piles-vec *piles-vec* 5) ;; => (5)
*piles-vec* ;; => #((1) (2) (3) (4) (5)) ← 変わっていないCode language: Lisp (lisp)
loop across も loop for in と同様に、要素をローカル変数 pile に束縛するので、(pop pile) はそのバインディングを書き換えるだけで、ベクターの要素スロットには届きません。
2. 再束縛できる「場所(place)」
pop はマクロで、実質的に次と同等です。
(pop x)
; (prog1 (car x)
(setf x (cdr x))) と同等Code language: Lisp (lisp)
Common Lisp では、setf に渡せる書き込み先を place と呼びます。
ローカル変数 x も place ですが、(car cell)、(cdr cell)、(aref vec idx) なども place です。
(setf x ...) ; ローカル変数のバインディングを変える
(setf (car cell) ...) ; コンスセルの car スロットを変える
(setf (aref vec idx) ...) ; ベクターの要素スロットを変えるCode language: Lisp (lisp)
pop はこの place を受け取るマクロなので、渡した place によって何が書き換わるかが決まります。
2.1. carは値を得るだけでなく場所を指す
「値を得る」と「値のある場所に書き込む」は別の操作です。
(pop (car cell))
; cell の car スロットが変わるCode language: Lisp (lisp)
(car cell) は値を返すだけでなく、setf の引数として渡すと「場所」として機能します。
しかし、ローカル変数に取り出した瞬間に、場所としての性質は失われます。
(let ((x (car cell)))
(pop x))
; x というバインディングが変わるだけ。
; cell には届かないCode language: Lisp (lisp)
(aref vec idx)も同様です。
(let ((aref vec idx))
(pop x))
; vec には届かないCode language: Lisp (lisp)
setf の対象が「ローカル変数 x のバインディング」か「コンスセルやベクターのスロット」かで、変更が届く範囲が変わるのです。
コンスセルのスロット(car や cdr)、あるいはベクターのスロット(aref)を直接書き換えれば、そのセルを参照している全変数の見え方が変わります。
3. 【修正】vector を添字でアクセスする(届く)
ベクターの要素スロットを書き換えるには、インデックスで aref を直接操作すればよいことになります。
(defun take-cards-in-piles-vec (piles card)
(loop for idx below (length piles)
for pile = (aref piles idx)
for pos = (position card pile)
when pos
return (loop repeat (1+ pos)
collect (pop (aref piles idx)))))
(take-cards-in-piles-vec *piles-vec* 5) ;; => (5)
*piles-vec* ;; => #((1) (2) (3) (4) NIL) ← 変わったCode language: Lisp (lisp)
(aref piles idx) はベクターの要素スロットへの参照なので、setf がそのスロットを直接書き換えます。
3.1. 【修正】list を loop for on で走査する(届く)
リストの場合も loop で書き換えるには、要素を走査してはダメです。
とはいえ、毎回、(nth idx piles) でアクセスすると重くなってしまうので、コンスセルそのものを受け取る、loop for on を使います。
(defun take-cards-in-piles (piles card)
(loop for cell on piles
for pile = (car cell)
for pos = (position card pile)
when pos
return (loop repeat (1+ pos)
collect (pop (car cell)))))
(take-cards-in-piles *piles* 5)
;; => (5)
*piles*
;; => ((1) (2) (3) (4) NIL) ← 変わったCode language: Lisp (lisp)
cell は piles のセルを直接指しているので、(setf (car cell) ...) が piles の該当スロットまで届きます。
4. まとめ(loopとスロット)
pop が書き換えるのは、渡した場所そのものです。
| 走査方法 | pop の引数 | 書き換わる場所 | 元の構造への影響 |
|---|---|---|---|
loop for in | ローカル変数 pile | バインディング | なし |
loop for on | (car cell) | コンスセルの car スロット | あり |
loop across | ローカル変数 pile | バインディング | なし |
loop for idx | (aref piles idx) | ベクターの要素スロット | あり |
loop for on と添字アクセスに共通するのは、コンテナの中の場所、つまり place を setf の対象にしているという点です。
ローカル変数に取り出した瞬間に、その場所との接続は切れます。
値を読むだけなら for in や across で十分です。
シーケンスそのものを書き換えたいなら、要素を変数に束縛するのではなく、スロットにアクセスできる place の形で持ち続ける必要があります。