第14章「リスト」

概要

ANSI Common Lispの第14章「コンス」を説明します。

コンスの基本

コンス」とは、2つのスロットで構成されるオブジェクトです。手前のスロットは CAR 部と呼ばれ、後ろのスロットは CDR 部と呼ばれます。

CAR 部は主に値を保持しますが、CDR 部は別の「コンス」への参照を保持するために使われます。もちろん CDR 部に値を設定することもできますが、CDR 部が別のコンスへの参照であれば、あるコンスから別のコンスへと参照先を変えていくことができます。

CDR 部を参照の連鎖にしてコンスを擬似的に連結すると、「リスト」構造を実装することができます。リスト構造は配列構造と異なり、一連のメモリ領域を必要としません。要素を追加したければメモリ上の適当な領域をコンスとして確保し、その CAR 部に追加の値を、CDR 部に既存のリスト構造への参照を設定します。するとリスト構造の最初に新しい値を追加することができます。

リスト構造の終端はnilを設定します。つまり、最後の要素の CDR 部をnilにして、どこへも参照しないようにします。

リスト構造はLISPにとって重要であり、最初期のLISPから存在していました。しかし、Common Lispが制定される頃には配列はもちろん、構造体やクラスなど様々なデータ構造がサポートされるようになりました。そのため、実際のプログラミングでリスト構造を選択すべき場合というのは意外と少ないと思います。ベクトルや構造体の方がランダムアクセスが高速であり、クラスの方が拡張性が高いため、リストをデータ構造として採用する場合というのは、「要素の数が比較的少ない場合」や「ランダムアクセスを行わない場合」などに限られます。

それでもやはり、リスト構造はCommon Lispにとって極めて重要です。なぜなら、Common Lispのソースコードは、それ自体がリスト構造として解析されるからです。ソースコードそのものが、簡素で柔軟なリスト構造で構築されているという事実こそが、LISPをLISPたらしめていると言ってもいいでしょう。その最大の恩恵は「マクロ」を使うことができるということであり、マクロはソースコードに対するリスト操作に他なりません。
この章ではリスト構造を中心に、コンスの扱いを紹介します。

なお、「リスト」と「コンス」は正確には異なる意味を持ちます。一般的に、CAR 部と CDR 部を持つデータ構造を「コンス」(コンスセル)と呼び、CDR 部を別のコンスへの参照にして連鎖させたものを「リスト」(リスト構造)と呼びます。

ただし、実際にはnilという特別な値が存在するため、そう単純ではありません。ANSI Common Lispの仕様では、nilは偽値であると同時に「空リスト」であるとされています。つまり、nilはシンボルであると同時にリストでもあります。そのため、listという型を親のクラスとして用意しており、サブクラスとしてコンスを表すcons型とnilを表すnull型を排他的関係で定義しています。つまり、list型とcons型の違いは、シンボルでもありリストでもあるnil(型としてはnull型)を含むか含まないか、ということになります。

list型とcons型の違い>
(typep nil 'cons)
; => NIL
(typep nil 'list)
; => T

;; nil の扱い以外は違いがない
(typep '(1 . 2) 'cons)
; => T
(typep '(1 . 2) 'list)
; => 

(eq nil '())
; => T

;; 多くの処理系では空リストを nil と表示する
'()
; => NIL

生成: cons

コンスを生成するにはcons関数を使います。
(cons 1 2)
; => (1 . 2)

(cons 1 nil)
; => (1)

(cons 2 (cons 1 nil))
; => (2 1)

(cons 'a (cons 'b (cons 'c nil)))
; => (A B C)

Common Lispでは、ドット.で区切られたカッコ付のオブジェクトはコンスであることを示します。そして、.のないカッコ付のオブジェクトは終端がnilになっているリストであることを示します。
(cons 1 (cons 2 3))
; => (1 2 . 3)

(cons 1 (cons 2 (cons 3 nil)))
; => (1 2 3)

上の例は最後のコンスの CDR 部が3のため、ドット.が示されています。他方、下の例は最後のコンスの CDR 部がnilのため、ドットはありません。これらは表面上同じように見えますが、次小節のcdr関数で参照する際に動作が異なります。

なお、consというシンプルな関数も、他の高階関数と組み合わせると様々な処理が可能です。例えば、リストを逆順に変更してみます。

まず、引数の順序を逆にしてコンスを作る関数を定義します。
(defun rcons (x y) (cons y x))
; => RCONS

あとはこの関数をreduceという畳み込みを行う高階関数に渡し、最初の引数としてnilを指定します。これだけでリストを逆順にすることができます。
(reduce #'rcons '(1 2 3 4) :initial-value nil)
; => (4 3 2 1)

参照: car, cdr

コンスの CAR 部を参照するにはcarアクセッサを、CDR 部を参照するにはcdrアクセッサを使用します。
(car '(1 2))
; => 1
(cdr '(1 2))
; => (2)

(car '(1 . 2))
; => 1
(cdr '(1 . 2))
; => 2

参照したいオブジェクトがリストの時、cdrCDR 部を参照すると一番最初の要素を除く残りの要素がリストの形式で取得されます。そのため、ドットがないリストの場合は返り値もまたリストになりますが、ドットのあるコンスなら値を直接取り出すことになります。

もっとも、ドットがあるかどうかは表記上の問題であり、重要なのは最後がnilになっているかどうかです。
(cdr '(1 2 3))
; => (2 3)
(cdr '(1 . (2 . (3))))
; => (2 3)
(cdr '(1 . (2 . (3 . nil))))
; => (2 3)

(cdr '(1 . (2 . 3)))
; => (2 . 3)

この例では、上の3つは全く同じ処理ですが、一番下のみ異なります。上の3つは明示的に、あるいは暗黙的に最後の要素がnilのリストを参照していますが、一番下の最後の要素は3です。

なお、carcdrを組み合わせれば、全ての要素を参照することができます。
(car '(1 2 3))
; => 1
(car (cdr '(1 2 3)))
; => 2
(car (cdr (cdr '(1 2 3))))
; => 3
carcdrconsを組み合わせば、元のリストから新しいリストを作ることもできます。
(cons 'a (cdr '(1 2 3)))
; => (A 2 3)
(cons (car '(1 2 3)) (cons 'b (cdr (cdr '(1 2 3)))))
; => (1 B 3)
(cons (car '(1 2 3)) (cons (car (cdr '(1 2 3))) (cons 'c nil)))
; => (1 2 C)

変更: rplaca, rplacd

リストの要素を変更するにはrplaca関数とrplacd関数を使います。これらの関数は第1引数に変更したいコンスを、第2引数に変更後の値を指定して使います。
(rplaca '(1 2 3) 'a)
; => (A 2 3)
(rplacd '(1 2 3) '(b 3))
; => (1 B 3)
(rplacd '(1 2 3) '(2 c))
; => (1 2 C)

(rplacd '(1 2 3) (rplaca (cdr '(1 2 3)) 'b))
; => (1 B 3)
(rplacd '(1 2 3) (rplacd (cdr '(1 2 3)) (cons 'c nil)))
; => (1 2 C)

これらの関数は副作用を目的として使うため、実際には上のような使い方はしません。多くの場合は、リストが束縛されている変数を書き換えるために使います。
(defparameter *cons* '(1 2 3))
; => *CONS*
*cons*
; => (1 2 3)

(rplaca *cons* 'a)
; => (A 2 3)
*cons*
; => (A 2 3)       ; 変数が変更されている

(rplacd *cons* (cons 'b (cdr (cdr *cons*))))
; => (A B 3)
(rplacd *cons* (cons (car (cdr *cons*)) (cons 'c nil)))
; => (A B C)

*cons*
; => (A B C)

マクロによる変更: setf

rplaca関数とrplacd関数を使うことでリストを変更することができますが、ANSI Common Lispにはsetfマクロがあるので、変更はsetfを使うことが推奨されます。
;; いったん変数を初期化する
(setq *cons* '(1 2 3))
; => (1 2 3)

;; car, cdr のアクセッサで参照するのと同じように変更できる
(setf (car *cons*) 'a)
; => A
*cons*
; => (A 2 3)

(setf (car (cdr *cons*)) 'b)
; => B
(setf (car (cdr (cdr *cons*))) 'c)
; => C

*cons*
; => (A B C)
(setf car)関数は多くの場合rplaca関数に展開され、(setf cdr)関数はrplacd関数に展開されますが、参照する場合と同じように操作できるので、setfマクロを介した方がより直感的でしょう。

以下はGNU CLISPでマクロ展開した例です。システムの内部関数として定義された%rplacaに展開されています。
(macroexpand-1 '(setf (car (cdr (cdr *cons*))) 'c))
; => (SYSTEM::%RPLACA (CDR (CDR *CONS*)) 'C) ;
;    T

リストに対する操作

この節では、リスト構造に対する操作を網羅的に扱います。

リストは「シーケンス」でもあるので、汎用的な操作は第17章「シーケンス」に規定された関数も使うことができます。例えばリストの長さを求めるlength関数はシーケンス一般に使えるので、この節には入っていません。ここで扱うのはシーケンスの中でも特にリストに特化した操作、ということになります。

生成: list

cons関数は2つの引数を取り、それぞれを CAR 部と CDR 部に設定したコンスを生成して返すものですが、コンスを用いたリスト構造のデータを作りたければ、list関数を使うのが手軽です。
(list 1 2 3)
; => (1 2 3)

(list 'a 'b 'c)
; => (A B C)

(equal (list 1 2 3) (cons 1 (cons 2 (cons 3 nil))))
; => T

もちろん、リテラル的にリスト構造を生成する場合は、quoteスペシャルオペレータを使うこともできます。
(quote (1 2 3))
; => (1 2 3)

'(1 2 3)
; => (1 2 3)

そのため、list関数を使うのが有効なのは、高階関数と共に使う場合などです。
(mapcar #'list '(1 4 7) '(2 5 8) '(3 6 9))
; => ((1 2 3) (4 5 6) (7 8 9))

(mapcan #'list '(1 4 7) '(2 5 8) '(3 6 9))
; => (1 2 3 4 5 6 7 8 9)

参照: first, rest, last, nth, nthcdr

ANSI Common Lispはリストの要素を参照するための方法が、carcdr以外にも数多く用意されています。最もよく使われるのは、1番目の要素から10番目までの要素に対応して定義されているfirst, second, third, ... , tenthアクセッサです。
(first '(1 2 3))
; => 1

(second '(1 2 3))
; => 2

(third '(1 2 3))
; => 3

これらはアクセッサとして定義されているので、setfマクロで使うことができます。つまり、参照と同時に変更も可能です。
(defparameter *list* '(1 2 3))
; => *LIST*

(setf (first *list*) 'a)
; => A
(setf (second *list*) 'b)
; => B
(setf (third *list*) 'c)
; => C

*list*
; => (A B C)

また、アクセッサは関数なので、高階関数で用いることもできます。
(mapcar #'second '((1 2 3)
                   (4 5 6)
                   (7 8 9)))
; => (2 5 8)

これらのアクセッサには亜種としてrestlastが定められています。restcdrと等価ですが、cdrがコンスに対して使うのに対して、restはコンスによるリスト構造に対して使われます。ただ、動作は全く同じなので、リストに対してcdrを使うこともよくあります。
(rest '(1 2 3))
; => (2 3)
lastはその言葉の通り最後の要素を参照するアクセッサですが、返り値は最後の要素を含むコンスになります。リスト構造の最後のコンスは CDR 部がnilなので、具体的には以下のような動作になります。
(last '(1 2 3))
; => (3)

(equal '(3) '(3 . nil))
; => T

常にリスト構造の最後の「要素」を取り出したければ、lastcarを組み合わせることになります。
(defun last* (list) (car (last list)))
; => LAST*

(last* '(1 2 3))
; => 3

また、より汎用的な要素参照としてnthアクセッサとnthcdr関数が定められています。これらは名前の通り「N番目の要素」と「N番目のコンスの CDR 部」を参照するものです。ただし、インデックス番号は0から始まるので、例えば(second x)(nth 1 x)と等価です。
(nth 1 '(1 2 3))
; => 2

(nthcdr 1 '(1 2 3))
; => (2 3)
nthfirst, second, ... , tenthが対応しているように、nthcdrにも対応している関数が存在しますが、こちらはあまり可読性が高くないので、高階関数で使うとき以外はあまり用いられないと思います。
(car '(1 2 3))
; => 1
(cadr '(1 2 3))
; => 2             ; = (car (cdr ...))
(caddr '(1 2 3))
; => 3             ; = (car (cdr (cdr ...)))

(cdr '(1 2 3))
; => (2 3)
(cddr '(1 2 3))
; => (3)           ; = (cdr (cdr ...))
(cdddr '(1 2 3))
; => NIL           ; = (cdr (cdr (cdr ...)))

;; 複雑だが強力な参照方法
(caar '((1 2 3)(4 5 6)(7 8 9)))
; => 1
(cadar '((1 2 3)(4 5 6)(7 8 9)))
; => 2
(caddar '((1 2 3)(4 5 6)(7 8 9)))
; => 3
(caadr '((1 2 3)(4 5 6)(7 8 9)))
; => 4
(cadadr '((1 2 3)(4 5 6)(7 8 9)))
; => 5

;; 参照可能なのは cXXXXr の最大6文字まで
(caddadr '((1 2 3)(4 5 6)(7 8 9)))
; *** - EVAL: undefined function CADDADR

連結: append, nconc

リストとリストを連結するには、append関数かnconc関数を用います。2つの関数の違いは、元のリストをコピーしてから新しい連結リストを作るか、元のリストを破壊・修正して連結リストにするかです。連結前のリストを再利用するならappendを、再利用しないならnconcを使うべきです。
;; 一見すると同じに見える append と nconc
(append '(1 2 3) '(4 5 6))
; => (1 2 3 4 5 6)
(nconc '(1 2 3) '(4 5 6))
; => (1 2 3 4 5 6)

;; append は元のリストをコピーするので、修正されない
(let ((a '(1 2 3))
      (b '(4 5 6)))
  (print (append a b))
  (print a)
  (print b)
  'append)
; (1 2 3 4 5 6)     ;; 連結リスト(appendの返り値)
; (1 2 3)           ;; a もそのまま
; (4 5 6)           ;; b もそのまま
; => APPEND

;; nconc は元のリストを修正してリストをつなぎ合わせる
(let ((a '(1 2 3))
      (b '(4 5 6)))
  (print (nconc a b))
  (print a)
  (print b)
  'nconc)
; (1 2 3 4 5 6)     ;; 連結リスト(nconcの返り値)
; (1 2 3 4 5 6)     ;; a は修正されている
; (4 5 6)           ;; b はそのまま
; => NCONC
nconcは引数を修正するという副作用が発生するため、うっかり油断するとバグの原因になることもありますが、リストをコピーするappendよりも高速に処理することができます。どちらを使うかは「元のリストを再利用するか」という点で判断しましょう。

追加・削除: push, pop, adjoin, pushnew

リストに要素を追加するのは簡単です。新しいコンスをcons関数で生成し、元のリストを CDR 部に、追加したい要素を CAR 部に設定すれば、リストに要素を追加することができます。
(defparameter *list* '())
; => *LIST*
*list*
; => NIL

(setq *list* (cons 'a *list*))
; => (A)
(setq *list* (cons 'b *list*))
; => (B A)
(setq *list* (cons 'c *list*))
; => (C B A)

*list*
; => (C B A)

そもそもリスト構造は一連のデータではなく、分散したコンスの CDR 部を使って擬似的に連結しているだけです。そのため、とても柔軟に要素を追加することができます。
pushマクロは上の処理、つまりconsによるコンスの生成とsetq(実際はsetfマクロ)による束縛を同時に行います。
(push 'd *list*)
; => (D C B A)
(push 'e *list*)
; => (E D C B A)

*list*
; => (E D C B A)

逆に、データを削除するのはcdrで2番目のコンス以降だけを参照すれば可能です。
(setq *list* (cdr *list*))
; => (D C B A)
(setq *list* (cdr *list*))
; => (C B A)

*list*
; => (C B A)
popマクロもpushマクロと同様に、cdrsetqを同時に行うマクロです。
(pop *list*)
; => C
(pop *list*)
; => B
(pop *list*)
; => A
(pop *list*)
; => NIL
pushpopはスタック構造(後入れ先出し)を簡便に扱うためのものです。ANSI Common Lispではさらに便利な関数とマクロが定められています。adjoin関数は、要素の重複がある場合は追加を行わず、重複がない場合のみ追加を行います。adjoinは関数なので、変数の変更は行わず、返り値を返すだけです。
(push 'a *list*)
; => (A)
(push 'b *list*)
; => (B A)
(push 'c *list*)
; => (C B A)

(adjoin 'd *list*)
; => (D C B A)
(adjoin 'b *list*)
; => (C B A)

*list*
; => (C B A)
adjoin関数を使って重複を避けながら変数(リスト)に要素を追加し、その返り値を元の変数に束縛し直す場合は、pushnewマクロを使います。
(pushnew 'd *list*)
; => (D C B A)
(pushnew 'b *list*)
; => (D C B A)

*list*
; => (D C B A)

なお、リストは構造的に手前に追加する方が計算量が少ないため、要素の追加がある場合は手前に追加します。手前に追加していったリストを逆順にするにはreverse関数を用います。
(reverse *list*)
; => (A B C D)

判定: atom, consp, listp, endp, null

コンスはCommon Lispにとって基本的なデータ構造のため、その判定に用いる述語関数も多く用意されています。

まず、コンスであるかどうかを確認するには、consp関数を用います。conspはその否定形であるatom関数も定められています。
(consp '(1 2 3))
; => T
(consp 1)
; => NIL

(atom '(1 2 3))
; => NIL
(atom 1)
; => T
conspはコンスであるかどうかを確認するので、空リスト('())に対して偽値を返します。
(consp '())
; => NIL

空リストに対して真値を返したい場合は、listp関数を用います。
(listp '())
; => T
listpconspの違いは、単に空リストに対して真を返すか偽を返すかの違いなので、もちろんコンスに対しては真を返します。
(listp '(1 2 3))
; => T

空リスト自体を判定したい場合はendp関数またはnull関数を用います。endp関数とnull関数は共に空リスト('())すなわち偽値(nil)に対していtを返しますが、endpは引数がリストではない場合にtype-error(エラー)を通知するのに対して、nullは全てのオブジェクトを引数とすることができ、単純にnilかどうかを判定します。
(null nil)
; => T
(endp nil)
; => T

(null 1)
; => NIL
(endp 1)
; *** - ENDP: A proper list must not end with 1

検索: member, member-if

リストの中にある要素が存在するかどうかを調べるにはmember関数を使います。この関数はリストをコンス単位で調べながら、探したい要素が CAR 部に一致する場合にそのコンスを返します。コンスを返すということは CDR_ 部も付いてくるので、その要素以降の全ての要素が返されます。
(member 2 '(1 2 3))
; => (2 3)

要素そのものを返り値としたい場合はfind関数を使います。こちらはリストだけでなくシーケンス(第17章)に対して定義されているので、文字列を含むベクトルにも使うことができます。
(find 2 '(1 2 3))
; => 2
(find 4 '(1 2 3))
; => NIL

(find #\i "common lisp")
; => #\i
(find #\a "common lisp")
; => NIL
memberは要素を探しますが、その判定にはeql述語関数が用いられます。述語を変更したい場合は、:testキーワード引数を使います。
(member 2 '(1.0 2.0 3.0))
; => NIL
(member 2 '(1.0 2.0 3.0) :test #'=)
; => (2.0 3.0)

また、要素を検索するだけでなく、条件に一致する要素が存在するかどうかを調べることもできます。高階関数として検索を行う場合はmember-if関数を用います。
(member-if #'evenp '(1 2 3 4 5))
; => (2 3 4 5)
(member-if #'(lambda (x) (zerop (mod x 3))) '(1 2 3 4 5))
; => (3 4 5)
member-ifには否定形のmember-if-notも存在しますが、何を調べたいのか分かりにくくなってしまうため、非推奨とされています。基本的にはmember-ifを用いてください。

置換: subst, nsubst

リストの要素を別の要素に置き換えるには、subst関数を用います。
(subst 4 2 '(1 2 3))
; => (1 4 3)

なお、substはリストをコピーして新しいリストを返しますが、コピーが不要でリストを再利用しない場合はnsubst関数を使うことができます。
substにも高階関数として用いるsubst-ifが定義されていますが、これは要素の置換を行うように設計されてはいないので、リストを要素単位で置換したい場合はシーケンスに対して定義されているsubstitute関数やsubstitute-if関数を用います。
(substitute 4 2 '(1 2 3))
; => (1 4 3)

(substitute-if 5 #'oddp '(1 2 3))
; => (5 2 5)

マッピング: mapc, mapcar, mapcan

一連のデータを1つ1つ取り出しながら、何らかの処理をしていくことは「マッピング」と呼ばれます。ANSI Common Lispでは最も汎用的なマッピング関数としてシーケンス用のmap関数が定められていますが、シーケンスの一種であるリストに対しては6つの関数が別に定められています。関数は6つですが、動作の種類としては3種類で、それぞれに対して CAR 部を対象とするか CDR 部とするかで2種類ずつ用意されているので6つあります。ここでは、CDR 部を対象とするものは紹介せず、CAR 部を対象にするもののみを紹介します。
Function: mapc
第2引数のリストの各要素に対して第1引数の関数を適用し、元のリストを返り値として返します。主に副作用を目的として用いられます。
Function: mapcar
第2引数のリストの各要素に対して第1引数の関数を適用し、返り値をリストにして返します。
Function: mapcan
第2引数のリストの各要素に対して第1引数の関数を適用し、返り値をnconcで連結して返します。
mapcは副作用を目的として用いられます。
(mapc #'print '(1 2 3))
; 1 
; 2 
; 3 
; => (1 2 3)
mapcarはシンプルなマッピング関数で、元のリストに対応するようなリストを返します。
(mapcar #'(lambda (x) (expt x 2)) '(1 2 3))
; => (1 4 9)
mapcanmapcarに似ていますが、マップした後の個々の要素を全てnconcで繋げて1つのリストにして返します。
(mapcan #'identity '((1 2 3) (4 5 6) (7 8 9)))
; => (1 2 3 4 5 6 7 8 9)

これは、以下と等価です。
(apply #'nconc (mapcar #'identity '((1 2 3) (4 5 6) (7 8 9))))
; => (1 2 3 4 5 6 7 8 9)

なお、これら3つの関数は全て CAR 部に対するものですが、CDR 部に対する関数はそれぞれmapl, maplist, mapconとして定められています。

連想リストに対する操作

リスト構造は要素の追加や削除が容易であり、とても柔軟なデータ構造です。そのため、リスト構造を基礎とした便利な使い方が考えられてきました。

「連想リスト」(Association List)はその使い方の一つです。ANSI Common Lispではハッシュテーブルが用意されていますが、ハッシュテーブルはキーからハッシュの計算を行うためランダムアクセスに高速ですが、非常に小規模なテーブルでも計算が発生します。連想リストはキーと値をコンスに設定して、複数のコンスをリストとしてまとめておく構造をしています。要素の数が少ない場合は十分に高速にアクセスできることに加え、全体がリストなのでリテラル表記(ソースコードの中にデータをそのまま記述すること)が可能なので、手軽なデータ構造として用いられます。

連想リストはここまでに紹介したオペレータで扱うこともできますが、連想リストに特化したオペレータもいくつか定められています。この節では、そのようなオペレータをいくつか紹介します。

追加: acons

まず、連想リストはリストなので、何も要素がない状態は空リストです。
(defparameter *alist* '())
; => *ALIST*

そして、連想リストの各要素は、'(key . value)というコンスになっているため、このようなコンスを用意してリストに追加すれば連想リストになります。
(push (cons 'url "lisp.satoshiweb.net") *alist*)
; => ((URL . "lisp.satoshiweb.net"))

「追加」の操作はこのようにpushマクロとcons関数でもできますが、aconsという関数として定められています。
(setq *alist* (acons 'since 2017 *alist*))
; => ((SINCE . 2017) (URL . "lisp.satoshiweb.net"))

(setq *alist* (acons 'author "Satoshi" *alist*))
; => ((AUTHOR . "Satoshi")
;     (SINCE . 2017)
;     (URL . "lisp.satoshiweb.net"))

キーによる参照: assoc, assoc-if

連想リストの各要素にアクセスするには、assocを使います。
(assoc 'since *alist*)
; => (SINCE . 2017)

高階関数として使いたい場合はassoc-if関数を使います。
(defun symbol-first-char (sym) (aref (symbol-name sym) 0))
; => SYMBOL-FIRST-CHAR

(assoc-if #'(lambda (x) (char= (symbol-first-char x) #\U)) *alist*)
; => (URL . "lisp.satoshiweb.net")

連想リストの特徴は、要素が必ずコンスであるという点です。つまり、コンスの CAR 部にはキーが、CDR 部には値が入っています。取り出した要素にcdrを適用すれば値を得られます。
(cdr (assoc 'since *alist*))
; => 2017

値による参照: rassoc, rassoc-if

一般には連想リストはキーから値を取り出すために使われますが、値からアクセスすることもできます。値からアクセスするにはrassoc関数やrassoc-if関数を使います。

しかし、値で検索しても、マッチするもの一つしか得られないので、あまり使い道はないかもしれません。
(rassoc-if #'numberp *alist*)
; => (SINCE . 2017)

(rassoc-if #'stringp *alist*)
; => (AUTHOR . "Satoshi")     ;; マッチしても1つしか取得できない

属性リストに対する操作

連想リストと同じように簡易的なハッシュテーブルとして用いられるのが「属性リスト」(Property List)です。

属性リストは第10章「シンボル」で紹介した通り、シンボルのセルの一つとして用意されています。しかし、当然ながらシンボルの属性として使う属性リストだけでなく、属性リスト単体でも使うことができます。

属性リストは連想リストよりも見た目はシンプルですが、使用するコンスの数は同じです。連想リストを使うか属性リストを使うかは好みの問題と言えるでしょう。私は属性リストの方がsetfマクロとも統合されているので使いやすいように感じています。また、下で紹介するgetfアクセッサはassoc関数と異なり、キーで検索すると値が直接得られます。極めてシンプルなデータ構造として、より直感的に使えると思いますので、皆さんも使ってみてください。

参照: getf

属性リストのためのgetf関数はアクセッサでもあるので、汎参照に使うことができます。
(defparameter *plist* '())
; => *PLIST*

(setf (getf *plist* 'url) "lisp.satoshiweb.net")
; => "lisp.satoshiweb.net"
(setf (getf *plist* 'since) 2017)
; => 2017
(setf (getf *plist* 'author) "Satoshi")
; => "Satoshi"

*plist*
; => (AUTHOR "Satoshi" SINCE 2017 URL "lisp.satoshiweb.net")

(getf *plist* 'url)
; => "lisp.satoshiweb.net"
(getf *plist* 'since)
; => 2017

削除: remf

属性リストには、キーと値を削除するオペレータも用意されています。それがremfマクロです。
(remf *plist* 'url)
; => T

*plist*
; => (AUTHOR "Satoshi" SINCE 2017)

(remf *plist* 'author)
; => T

*plist*
; => (SINCE 2017)

0 件のコメント :

コメントを投稿