Hatena::Groupcadr

わだばLisperになる このページをアンテナに追加 RSSフィード

2004 | 12 |
2005 | 01 | 02 | 07 | 10 | 11 |
2006 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 11 |

2007-12-16

Practical Common Lisp (8)

| 21:30 | Practical Common Lisp (8) - わだばLisperになる を含むブックマーク はてなブックマーク - Practical Common Lisp (8) - わだばLisperになる

引き続きPractical Common Lisp 第三章3. Practical: A Simple Databaseを読んでみます。

Removing Duplication and Winning Big

この段落は、殆ど文章で、マクロの解説のようなので、文章を纏めてメモすることにします。

  • データの登録、更新、削除、保存、読み出しと一通り作成できた。大体50行位にまとまった。
  • しかし、これまでのコードにはコードの重複があって、ちょっと気になる。
  • 具体的には、whereの中で、(if title (equal (getf cd :title) title) t)の様にif式がずらっと並んでるのが気になる。updateもそんなところがある。
  • コードの重複で良くないところは、コードを追加/修正する場合、散らばった個所を一つずつ修正しなくてはいけないところ。
  • whereについて言えば、予め決めたキーワードを設定し、本体にも相応するキーワードを埋め込んでいるが、これも修正時には手間。
  • whereの実行時に動的に展開することによって解決できないか。
(select (where :title "Give Us a Break" :ripped t))

という表現が、実行時に

(select
 #'(lambda (cd)
     (and (equal (getf cd :title) "Give Us a Break")
          (equal (getf cd :ripped) t))))

と変換されるとすれば可能。

  • ここでマクロ登場
  • Common Lispのマクロは、C等の文字列置換がベースのプリプロセッサとは違い、コンパイラがコンパイル時にマクロが書かれた場所に展開してからLispの式として評価する。
  • 関数とマクロは全く違った効果を持つもの
  • 簡単なマクロの例
(defmacro backwards (expr) (reverse expr))

CL-USER> (backwards ("hello, world" t format))
hello, world
NIL
  • 動作の説明
    • ("hello, world" t format)という引数で、マクロの本体を評価する。マクロの引数は評価されない:(reverse '("hello, world" t format))のような感じ
    • '(format t "hello, world")が展開された式となる
    • 展開された式をもとにマクロがあった文脈でREPLが実行
  • 謂わば、上で定義したbackwardsは、引っくり返ったLisp式を実行する新しいLisp言語を定義したようなもの。そして、それは、通常の式に展開されるので、普通に書いたコードと効果は何等変わらず、まったく同じにコンパイルされる。
  • それで、問題であったwhareにはこのことがどう使えるか→キーワード毎に(equal (getf cd field) value)というような式が生成され、それが実行されるようになれば良い。
(defun make-comparison-expr (field value)    ; 駄目
  (list equal (list getf cd field) value))

ではどうか。

  • equalや、getf、cd、value等全部評価されることになる。fieldとvaluesは評価されても良いが、equalや、getf、cdは評価されては困る→quote(')を使う。
(defun make-comparison-expr (field value)
  (list 'equal (list 'getf 'cd field) value))
  • 式は生成できるようになったが、もっと読み書きしやすい方法がある。式の評価する部分としない部分をわかりやすく書くことができ、それには、バッククウォート(`)とコンマ(,)を組合せて使う。
`(1 2 (- 1 2))        ==> (1 2 (- 1 2))
`(1 2 ,(- 1 2))       ==> (1 2 3)

という風に書ける。

バッククオートを使うと、make-comparison-exprは

(defun make-comparison-expr (field value)
  `(equal (getf cd ,field) ,value))

とわかりやすく書ける。

  • 以前に定義したwhereを振り返ると、キーワードを判定する式がANDで纏められていた。とりあえず、複数のキーワードを処理する節を纏めてリストにして返す関数は、
(defun make-comparisons-list (fields)
  (loop while fields
     collecting (make-comparison-expr (pop fields) (pop fields))))

と書ける。

  • 新しくLOOPがでてきたが22章で詳しく解説する。また、POPはPUSHの反対の作用をする関数。これらを纏めて新しいwhereをマクロで定義できる
(defmacro where (&rest clauses)
  `#'(lambda (cd) (and ,@(make-comparisons-list clauses))))
  • ,@の解説
    • ,@は,の仲間でリストの中身をスプライスする。,@はリストの最後に限らずどこに位置していても構わない
`(and ,(list 1 2 3))   ==> (AND (1 2 3))
`(and ,@(list 1 2 3))  ==> (AND 1 2 3)
  • 引数部の&restパラメータの説明
    • &keyに似ているが、&restは複数の引数を纏めて一つのリストとして扱えるようにする。
(where :title "Give Us a Break" :ripped t)

という引数が与えられた場合、変数clauseの中身は

(:title "Give Us a Break" :ripped t)

となる。

  • 定義したマクロがどのように展開されるかを確認するには、MACROEXPAND-1を使う
CL-USER> (macroexpand-1 '(where :title "Give Us a Break" :ripped t))
#'(LAMBDA (CD)
    (AND (EQUAL (GETF CD :TITLE) "Give Us a Break")
         (EQUAL (GETF CD :RIPPED) T)))
T

Wrapping Up

  • ということで、コードの重複を除去することに成功したが、同時に、よりコードを一般化できたことに気付く
  • マクロというのは、抽象化の一つで、構文上の抽象化といえる
  • 今のところは、make-cd、 prompt-for-cd、add-cdといった関数と一緒に使っているだけだが、whereマクロ自体は、plistベースのデータベース一般で使うことができるものになった。
  • 作成したデータベースは、完全なものには程遠いが、色々な機能の追加をあれこれ考えてみることができるだろう。複数のテーブルを扱ったり、クエリーの方法に凝ってみる等。27章で作るMP3データベースでそういう機能を実装してみたい。

Practical Common Lisp (7)

| 18:06 | Practical Common Lisp (7) - わだばLisperになる を含むブックマーク はてなブックマーク - Practical Common Lisp (7) - わだばLisperになる

Updating Existing Records--Another Use for WHERE

引き続きPractical Common Lisp 第三章3. Practical: A Simple Databaseを読んでみています。
とりあえず出てきたコードを暗記して再現して感想をメモ
(defun update (selector-fn &key artist title rating (ripped nil ripped-p))
  (setf *db*
	(mapcar (lambda (row)
		  (when (funcall selector-fn row)
		    (if artist (setf (getf row :artist) artist))
		    (if title (setf (getf row :title) title))
		    (if rating (setf (getf row :rating) rating))
		    (if ripped-p (setf (getf row :ripped) ripped)))
		  row)
		*db*)))
できた。今度はデータの内容を更新するものを作成する様子。
(defun delete-row (selector-fn)
  (setf *db* (remove-if selector-fn *db*)))
できた。データを削除するものらしい。
以上を踏まえて本文を読んでみる
  1. データベースにはデータを更新する機能が必須なので作る。
  2. これも、SQLに倣ってupdateという名前で作成することにして、データの抽出には定義したwhereを使用する。
  3. MAPCARの説明
    • リストの中の一つ一つのアイテムを総嘗めで処理するのには、mapcarが使える。
  4. SETFの説明。
    • (setf (getf row :ripped) ripped)のような形式が初めてでてきた。ちょっとややこしいが、詳細は、6章で説明する。とりあえず、setfは、変数の名前だけでなく、「変数が格納されている場所」を指定することにより、その場所に値を格納することができるとでも思っておく。
  5. REMOVE-IF/REMOVE-IF-NOTの説明
    • REMOVE-ifは条件を判定する関数とリストを引数に取り、リストのアイテムを一つずつ調べ、条件が満されたアイテムが含まれないリストを返す。REMOVE-IF-NOTは、条件を満さないもののリストを返す。つまり、リストから不要なものを取り除いた結果を返す。