コンスセルと place アイコン 2つのコンスセルが矢印でつながり、スロットへの書き込みを表すアイコン 【Common Lisp】
コンスセルは部分リストでは
なくスロットでもある
(リストとpop)

  • pop はマクロで、渡した place を直接書き換えるため、ローカル変数に取り出してから pop しても元のリストには届かない
  • loop for inloop 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 の各 carpile というローカル変数に束縛して走査します。
(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 acrossloop 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 のバインディング」か「コンスセルやベクターのスロット」かで、変更が届く範囲が変わるのです。

コンスセルのスロット(carcdr)、あるいはベクターのスロット(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)

cellpiles のセルを直接指しているので、(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 inacross で十分です。
シーケンスそのものを書き換えたいなら、要素を変数に束縛するのではなく、スロットにアクセスできる place の形で持ち続ける必要があります。