プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(53)

久しぶりです。年末いかがお過ごしでしょうか。
自分はというと彼女にフラれたりしていました。
「実践Common Lisp」やっていきます。

ルックアップテーブル

木、集合だけでなく、キーと値を対応させるテーブルもコンスセルで構築できるそう。
まあ、見たことがありますねalistは。あと、属性リストというのもあるそう。
後者は少なくとも名前を意識して使ったことが無い気がします。
どちらとも、巨大なテーブルとして使うことはなくとも、小さなテーブルとして使えば
ハッシュテーブルより効率的で、それぞれ固有の便利な能力が役立つそうです。

連想リスト

連想リストはキーと値を対応させるデータ構造で、逆方向の参照もサポートします。
とはいえ結局は要素がコンスセルになっているリストでしかありません。
carがキーを保持してcdrが値を保持しています。
指定するキーを持つリストを取り出すのはassocです。ちょっと懐しい。

CL-USER> (assoc 'cat '((cat . neko) (dog . inu) (human . hito)))
(CAT . NEKO)
CL-USER>

もはやこういう関数ではおなじみな感じですが、引数のキーと連想リスト中のキーを
比較する関数はキーワード引数:key,:testで変更できます。デフォルトはeqlです

CL-USER> (assoc "cat" '(("cat" . neko) ("dog" . inu) ("human" . hito)))
NIL
CL-USER> (assoc "cat" '(("cat" . neko) ("dog" . inu) ("human" . hito)) :test #'string=)
("cat" . NEKO)
CL-USER>

assocはリストの先頭から検索するため、リストの一番前に既にある要素を持つコンスセルを
追加することで値を更新したように見せかけることができます。
コンスセルの追加はconsを使ってもよいし、もっと便利なaconsというのもあるそう。

CL-USER> (defparameter *alist* '((cat . neko) (dog . inu) (human . hito)))
*ALIST*
CL-USER> (cons (cons 'cat 'nyan) *alist*)
((CAT . NYAN) (CAT . NEKO) (DOG . INU) (HUMAN . HITO))
CL-USER> (acons 'cat 'nyan *alist*)
((CAT . NYAN) (CAT . NEKO) (DOG . INU) (HUMAN . HITO))
CL-USER>

aconsも破壊的なものではありません。もし変更したければ下記のようにしなくてはなりません。

CL-USER> ;(setf *alist* (acons 'cat 'nyan *alist*)) ;こう書くか
; No value
CL-USER> (push (cons 'cat 'nyan) *alist*) ;こうする
((CAT . NYAN) (CAT . NEKO) (DOG . INU) (HUMAN . HITO))
CL-USER>

サイズが小さいテーブルならばハッシュテーブル以上のパフォーマンスを出すことができます。
さらに連想リストには比較関数を変更する以上の検索の柔軟性があります。
assoc-if, assoc-if-notがあり、テスト関数を渡すことによって、コンスセルのcarが
その関数を満たす最初のキーと値のペアを返してくれるそうです。
そしてrassocシリーズは、cdrを対象としての検索をしてくれるようです。

copy-alistという、リスト構造を構成するコンスセルを複製、
それらのcarが直接参照しているコンスセルの複製をしてくれる関数もあるということ。
あと、pairlisという面白そうな関数も。

CL-USER> (pairlis '(neko hito inu) '(cat human dog))
((INU . DOG) (HITO . HUMAN) (NEKO . CAT))
CL-USER>

2つのリストをalistにしてくれます。

属性リスト

属性リスト、plistはキーと値が交互に並ぶ、見た目は普通のリストなルックアップテーブルです。

CL-USER> '(INU  DOG HITO  HUMAN NEKO CAT)
(INU DOG HITO HUMAN NEKO CAT)
CL-USER>

基本的なルックアップ操作を行う関数はgetfしかないなど柔軟性はalistに劣るそう。
getfは:testでテスト関数を変更することができず、eqでの比較しかできないため、
キーとして使っていいのはシンボルだけです。キーが見つからないときにはnilが返りますが、
オプションで第三の引数を渡してそれを返してもらうこともできるということです。

CL-USER> (getf  '(INU  DOG HITO  HUMAN NEKO CAT) 'neko)
CAT
CL-USER> (getf  '(INU  DOG HITO  HUMAN NEKO CAT) 'niko)
NIL
CL-USER> (getf  '(INU  DOG HITO  HUMAN NEKO CAT) 'niko 'mew)
MEW
CL-USER>

setfで値の設定も可能。キーと値のセットを取り除くにはremfを使います。

CL-USER> (defparameter *plist* '(INU  DOG HITO  HUMAN NEKO CAT))
*PLIST*
CL-USER> (setf (getf *plist* 'neko) 'nyanko)
NYANKO
CL-USER> *plist*
(INU DOG HITO HUMAN NEKO NYANKO)
CL-USER> (remf *plist* 'hito)
T
CL-USER> *plist*
(INU DOG NEKO NYANKO)
CL-USER>

plistを使用していると、同一のリストから複数の属性を取り出したいということがあり、
それはget-propertiesという関数で可能であるそうです。
これは1つのplistと複数のキーを含むリストを受取り、最初に見つかったキーと対応する値、
それと見つかったキーから始まるリストの先頭を多値として返してくれるとのこと。
上手くイメージできないので試してみます。

CL-USER> (get-properties *plist* '(inu neko))
INU
DOG
(INU DOG NEKO NYANKO)
CL-USER> (get-properties *plist* '(neko inu))
INU
DOG
(INU DOG NEKO NYANKO)
CL-USER>

ふむ。欲しいキーと値を全てくれるような関数を考えます。

CL-USER> (defun all-val-prop (plist keys)
           (if (atom plist)
               plist
               (progn (multiple-value-bind (key value tail) (get-properties plist keys)
                         (cons (cons key value) (all-val-prop (cddr tail) keys))))))
ALL-VAL-PROP
CL-USER> (all-val-prop *plist* '(inu neko))
((INU . DOG) (NEKO . NYANKO))
CL-USER> (all-val-prop *plist* '(inu niko))
((INU . DOG) (NIL))
CL-USER>

ふう。傷心の青年が久し振りに書いたと思えばこんなものでしょう(うじうじ)。
prognが要りませんが、書き始めたときは必要になる気がしたのでした。
multiple-value-bindが全体を囲んでくれることが頭になかった。

plistについて、最後にシンボルとの関係を説明してくれるそうです。
どんなシンボルオブジェクトにも専用のplistがあり、そのシンボルの情報を格納するのに使用できるそう。
その専用plistはsymbol-plist関数で取得できるそうです。

CL-USER> (setf (symbol-plist 'neko) '(cat mew))
(CAT MEW)
CL-USER> (symbol-plist 'neko)
(CAT MEW)
CL-USER> (getf (symbol-plist 'neko) 'cat)
MEW
CL-USER> (get 'neko 'cat)
MEW
CL-USER>

getはgetfを簡略に書くことができるものだそう。setfも同様に可能だそう。
シンボルのplistから属性を取り除くときはsymbol-plistをremfしてもいいし、
rempropというこれまたgetのように簡単に書くことができる関数を使ってもいいということ。

CL-USER> (remprop 'neko 'cat)
(CAT MEW)
CL-USER> (get 'neko 'cat)
NIL
CL-USER>

このシンボルにどんな情報でも貼りつけられるということは、
シンボルを使うならもういつでも嬉しいことだそう。
まあ、損はしないですよね、きっと。24章で実例が出てくるそうなので、気長に待ちます。

destructuring-bind

destructuring-bindというのは、リストをスライスしたり、さいの目に切るマクロだそう。
基本的な枠組みは下記のようなもの。

(destructuring-bind (parameter) list body-form)

parameterにはオプショナルもレストもキーも含められるとのこと。
どのparameterもネストした分配パラメーターのリストで置き換えてよく、
その時は置き換える前のparameterに束縛されるはずだったリストも分解されるそう。
listは一回だけ評価され、その結果はリストでなくてはだめ。
その結果のリストが分配されて、parameterのリストの各変数に適切な値が束縛されます。
そうしたらbody-formが順に評価されていくということ。
よくわからないので、例をぽちぽち打ち込んでいきます。

CL-USER> (destructuring-bind (x y z) (list 1 2 3) (list :x x :y y :z z))
(:X 1 :Y 2 :Z 3)
CL-USER> (destructuring-bind (x y z) (list 1 (list 2 20) 3) (list :x x :y y :z z))
(:X 1 :Y (2 20) :Z 3)
CL-USER> (destructuring-bind (x (y1 y2) z) (list 1 (list 2 20) 3) (list :x x :y1 y1 :y2 y2 :z z))
(:X 1 :Y1 2 :Y2 20 :Z 3)

大体わかったような気がします。これにあと各種パラメータが使えるわけですね。

あと、destructuring-bindには&wholeというパラメータも使えるそうです。
&wholeにはリストのフォーム全体が束縛されるそう。
&wholeを使ったあとは、他のパラメータを何事もなかったかのように続けられるということです。

CL-USER> (destructuring-bind (&whole whole &key x y z) (list :z 3 :x 1 :y 2)
           (list :x x :y y :z z :whole whole))
(:X 1 :Y 2 :Z 3 :WHOLE (:Z 3 :X 1 :Y 2))
CL-USER>

これもまだ自分には使い所がわかりません。
31章でばっちし使うとのこと。気長に待ちます。

ふう。これで第13章、コンスセルについての章が完了しました。
次は「Land of Lisp」に戻ろうかなと思います。

寒いので風邪を引かないように気をつけましょう。おやすみなさい。