Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save otaon/7fc4e1cf0e24928b4ddd0b96dda7a47d to your computer and use it in GitHub Desktop.
Save otaon/7fc4e1cf0e24928b4ddd0b96dda7a47d to your computer and use it in GitHub Desktop.
実践 Common Lisp

ドキュメント一覧

  • Book "実践 Common Lisp"
    • 第1章 序論:なぜLispなのか?
    • 第2章 お気の済むまで:REPLツアー
    • 第3章 実践:簡単なデータベース
    • 第4章 シンタックスとセマンティクス
    • 第5章 関数
    • 第6章 変数
    • 第7章 マクロ:標準的な制御構文の構築
    • 第8章 マクロ:自分で定義しよう
    • 第9章 実践:ユニットテストフレームワーク
    • 第10章 数字、文字、そして文字列
    • 第11章 コレクション
    • 第12章 リスト処理:やつらがLISPと呼ぶ理由
    • 第13章 リストを越えて:コンスセルの別用途
    • 第14章 ファイルとファイルI/O
    • 第15章 実践:パスネーム可搬ライブラリ
    • 第16章 オブジェクト指向再入門:総称関数
    • 第17章 オブジェクト指向再入門:クラス
    • 第18章 FORMATの手習い
    • 第19章 例外処理を越えて:コンディションと再起動
    • 第20章 特殊なオペレータ
    • 第21章 大域的プログラミング:パッケージとシンボル
    • 第22章 黒帯のためのLOOP
    • 第23章 実践:スパムフィルタ
    • 第24章 実践:バイナリファイルのパース
    • 第25章 実践:ID3パーサ
    • 第26章 実践:AllegroServeでWebプログラミング
    • 第27章 実践:MP3データベース
    • 第28章 実践:Shoutcastサーバ
    • 第29章 実践:MP3ブラウザ
    • 第30章 実践:HTML生成ライブラリ ― インタプリタ版
    • 第31章 実践:HTML生成ライブラリ ― コンパイラ版
    • 第32章 結論:さて次は?
    • 付録A 日本語処理
    • 付録B 訳語一覧

第3章 実践:簡単なデータベース

ここでは、CD情報をデータベースのレコードとして残すコードを実装しながら、Lispの基本的なデータ操作方法を学ぶ。

3.1 CDとレコード

リッピング対象のCDの情報を管理するため、下記の情報を管理するデータベースを作成する。

  • CDタイトル
  • アーティスト
  • CDのレーティング
  • リッピング完了したか

CDのデータベースには、keyとvalueの組み合わせを持つリストを使うのが良さそうである。
Lispでは、連想配列の実現方法として主に2種類のデータ構造が使える。

名称 データ構造 コード例
連想リスト(association list, alist) keyとvalueのドットペアをリストの要素に持つ '((key1 . value1) (key2 . value2) ...)
属性リスト(property list, plist) keyとvalueを交互にリストの要素に持つ '(key1 value1 key2 value2 ...)

ここでは、属性リストを採用する。

CDのレコード情報を取り、CDを表す属性リストを返す関数を定義する。

(defun make-cd (title artist rating ripped)
  "CDを表す属性リストを返す
   title:  CDタイトル
   artist: アーティスト名
   rating: CDのレーティング
   ripped: リッピング完了したか
   ret: CDを表す属性リスト"
  (list :title title :artist artist :rating rating :ripped ripped))

3.2 CDのファイリング

複数のレコードを保持するデータ構造には、リストを採用する。

ダイナミック変数として*db*を定義する。

;; CDデータベース
(defparameter *db* nil)

レコードを*db*に格納する関数を定義する。

(add-record (cd)
  "CD情報をデータベースに追加する
   cd: CDを表す属性リスト
   ret: CD情報追加後のCDデータベース"
  (push cd *db*))

3.3 データベースの中身を見てみる

CD情報のデータベースを全てダンプする関数を定義する。
この関数では、format関数のフォーマット指定子によって、複数のレコードを1行で表示する。

(defun dump-db ()
  "CDデータベースの内容をダンプする
  ret: -"
  ;; 1レコード分の表示例
  ;;   TITLE:  Syro
  ;;   ARTIST: Aphex Twin
  ;;   RATING: 3
  ;;   RIPPED: T
  (format t "~{~{~a:~10t~a~%~}~%~}" *db*))

3.4 ユーザインタラクションを改善する

ユーザフレンドリなレコード追加仕組みを作成する。

ユーザプロンプトを表示して、標準ストリームへの入力を促す関数を定義する。

(defun prompt-read (prompt)
  "prompt: ユーザ入力画面のプロンプト文字
   ret: ユーザが入力したデータ1行"
  ;; 標準I/Oストリームを*query-io*に接続し、promptの値を流し込む
  (format *query-io* "~a: " prompt)
  ;; *query-io*ストリームを、改行コードがなくても表示する
  (force-output *query-io*)
  ;; ユーザに*query-io*ストリームへの入力を促す
  (read-line *query-io*))

上記の、ユーザへの入力を促す機能を利用して、ユーザにCD1枚分のデータを入力させる関数を定義する。

(defun prompt-for-cd ()
  "ユーザにCD1枚分のデータを入力させる
   ret: CDを表す属性リスト"
  (make-cd
    ;; CDタイトル
    (prompt-read "Title: ")
	;; アーティスト名
    (prompt-read "Artist: ")
	;; CDのレーティング
    (or
	  ;; レーティングは数値で取得する
	  ;; レーティングデフォルト値: 0
	  (parse-integer (prompt-read "Rating: ") :junk-allowed t)
	  0)
	;; リッピング完了したか
    (y-or-n-p "Ripped [y/n]: ")))

上記の、ユーザにCD1枚分のデータを入力させる関数を利用して、ユーザにCDのデータを好きなだけ入力させる関数を定義する。

(defun add-cds ()
  "ユーザにCD情報を登録させる
   ret: -"
  (loop (add-record (prompt-for-cd))
    (if (not (y-or-n-p "Anther? [y/n]: "))
	    (return))))

3.5 データベースの保存と読み出し

データベースの保存では、単純に*db*の中身をファイルに書き出すことにする。
あとでファイルから変数に代入できるように、あとからREPLが読み直す事のできるprint関数を定義する。

(defun save-db (filename)
  "指定したファイルにデータベースの中身を保存する
   filename: 保存先のファイルパス
   ret: -"
  (with-open-file (out filename
                   :direction :output      ; 出力先とする
				   :if-exists :supersede)  ; 既存のファイルを上書き
    (with-standard-io-syntax  ; print関数に影響する変数を標準に保つ
	  (print *db* out))))     ; REPLで読み直せる形でファイルに書き出す

ファイルに書き出したデータベースの情報を*bd*に代入する関数を定義する。

(defun load-db (filename)
  "ファイルに書き込まれたデータベースを*db*に読み込む
   filename: CDデータベースが保存されたファイルパス
   ret: -"
  (with-open-file (in filename) ; inにfilenameの内容を入力する
    (with-standard-io-sytax		; read関数に影響する変数を標準に保つ
	  (setf *db* (read in)))))

3.6 データベースにクエリを投げる

特定のアーティストのレコードのみを取得したい場合、remove-if-not関数を利用することが簡単に実現できる。

(defun select-by-artist (artist)
  "指定したアーティストのレコードのみ返す
   artist: アーティスト名
   ret: レコードのリスト"
  (remove-if-not
    #'(lambda (cd)
	    (equal (getf cd :artist) artist))
	artist))

これを応用して、任意のアトリビュートに対して値を指定してレコードを取得する関数を定義する。
タイトル指定用の関数、アーティスト名指定用の関数を全てのアトリビュート分用意すれば、全てのアトリビュートに対して絞り込みをかけることができる。

select

(defun select (selector-fn)
  "データベースからアトリビュート指定用の関数が真となるレコードのみ返す
   selector-fn: レコードの検索条件を満たすか判定する関数(セレクタ)
   ret: レコードのリスト"
   (remove-if-not selector-fn *bd*))

しかしながら、全てのアトリビュートに対して指定用の関数を定義するのは拡張性や保守性が悪くなる。
そこで、キーワードパラメータを使用する。キーワードパラメータとは&keyを使用した引数�の指定方法で、これを使用すると関数の引数の個数に依存されることがなくなる。

(defun foo (&key a b c)
  (list a b c))

このfooは、下記のように呼び出せる。
見ての通り、&keyに続く仮引数の変数は、引数におけるキーワードに続く値により束縛されている。
また、引数の値はキーワードとの対応のみによって仮引数に関連付けられており、引数の位置は全く関係しない。

> (foo :a 1 :b 2 :c 3)
(1 2 3)
> (foo :c 3 :b 2 :a 1)
(1 2 3)
> (foo :a 1 :c 3)
(1 NIL 3)
> (foo)
(NIL NIL NIL)

また、&keyの後に定義されたキーワードパラメータでは、単純にキーワードパラメータを指定するだけではなく、
(キーワードパラメータ名 デフォルト値 キーワード引数が与えられたかを表す変数)
という値を指定できる。

> (defun foo (&key a (b 20) (c 30 c-p))
    (list a b c c-p))

> (foo :a 1 :b 2 :c 3)
(1 2 3 T)
> (foo :c 3 :b 2 :a 1)
(1 2 3 T)
> (foo :a 1 :c 3)
(1 20 3 T)
> (foo)
(NIL 20 30 NIL)

この&keyを利用して、SQLなどのwhereのような機能を実現する。
where句に相当する関数を返す関数を定義している。 where関数が返す無名関数は、titleartistratingrippedをwhere関数から保持している。(クロージャ)

where

(defun where (&key title artist rating (ripped nil ripped-p))
  "レコードの検索条件を満たすか判定する関数を返す
   &key
   title: タイトル
   artist: アーティスト名
   rating: CDのレーティング
   ripped: リッピング完了したか
   ret: 引数で指定した条件を判定する関数"
  #'(lambda (cd)
      ;; レコードが指定された検索条件に合致するか判断する
      ;; cd: 対象レコード
      ;; ret: レコードが指定された検索条件に合致するか
      (and
        (if title 
          (equal (getf cd :title) title)
          t)
        (if artist
          (equal (getf cd :artist) artist)
          t)
        (if rating
          (equal (getf cd :rating) rating)
          t)
        (if ripped-p
          (equal (getf cd :ripped) ripped)
          t))))

キーワードパラメータrippedについては、(ripped nil ripped-p)という3つの要素を持つリストを指定している。
これにより、where関数のripped引数について下記を区別できるようになる。

  • whereを呼び出した側が、「フィールドrippedの値がnilのCDを選び出せ」という意味で:ripped nilと指定した
  • whereを呼び出した側が、「フィールドrippedの値は任意」という意味で、:rippedを指定しなかった

これを実行すると、下記のようになる。

;;; アーティスト名を指定してレコードを取得する
> (select (where :artist "Dixie Chicks"))
;;; レーティングとリッピング歴を指定してレコードを取得する
> (select (where :rating 10 :ripped nil))

3.7 既存のレコードを更新する - もう1つのWHEREの使い方

ここまでで、SQLクエリのselectwhere句に相当する関数を定義できた。

次に、SQLクエリのupdate句に相当する関数を定義する。

update

(defun update (selector-fn &key title artist rating (ripped nil ripped-p))
  "特定のレコードを更新する
   selector-fn: レコードの検索条件を満たすか判定する関数(セレクタ)
   &key
   title: 更新後のタイトル
   artist: 更新後のアーティスト名
   rating: 更新後のレーティング
   ripped: 更新後のリッピング歴
   ret: -"
  (setf *db*
        (mapcar
          #'(lambda (row)
              ;; セレクタで条件にマッチするレコード(row)のみ対象とする
              (when (funcall selector-fn row)
                (if title
                    ;; タイトルが指定されている場合、タイトルを更新
                    (setf (getf row :title) title))
                (if artist
                    ;; アーティストが指定されている場合、アーティストを更新
                    (setf (getf row :artist) artist))
                (if rating
                    ;; レーティングが指定されている場合、レーティングを更新
                    (setf (getf row :rating) rating))
                (if ripped-p
                    ;; リッピング歴が指定されている場合、リッピング歴を更新
                    (setf (getf row :ripped) ripped)))
              row)
          *db*)))

updateは下記のように使用できる。

;;; update前
> (select (where :artist "Dixie Chicks"))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 0 :Ripped T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 0 :Ripped T))

> (update (where :artist "Dixie Chicks") :rating 11)
NIL

;;; update後
> (select (where :artist "Dixie Chicks"))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 11 :Ripped T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 11 :Ripped T))

delete関数を定義するには、下記のようにする。

delete

(defun delete-rows (selector-fn)
  "特定のレコードを削除する
   selector-fn:  レコードの検索条件を満たすか判定する関数(セレクタ)
   ret: -"
   ;; 古い*db*から値を削除したデータベースを新しい*db*に代入する
  (setf *db* (remove-if selector-fn *db*)))

3.8 ムダを排除して勝利を収める

ここまでで、selectwhereupdatedeleteは、全て合わせて50行足らずで定義できた。
しかしながら、コード上にはまだ重複が存在するので、それを取り除くことにする。

修正対象はwhere関数である。

(defun where (&key title artist rating (ripped nil ripped-p))
      (and
        (if title 
          (equal (getf cd :title) title)
          t)
        (if artist
          (equal (getf cd :artist) artist)
          t)
        (if rating
          (equal (getf cd :rating) rating)
          t)
        (if ripped-p
          (equal (getf cd :ripped) ripped)
          t))))

where関数の下記の部分は、フィールドごとに何度も出てくる。

        (if title 
          (equal (getf cd :title) title)
          t)

このようなコードの重複は、フィールド構成を変更するときにコードの修正量が多くなるという欠点がある。 そこで、この部分をどうにか短縮することを考える。

まず、whereで実現したいことの本質は、「意図したフィールドの値をチェックするコードを生成する」だけである。
例えば、下記のselectの呼び出しコードを考える。

> (select (where :title "Give Us a Break" :ripped t))

このコードのwhere部分は、下記のように書き換えられる。

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

つまり、whereで返される関数内で行われているムダな処理(=引数に渡されていないフィールドの有無まで確認する処理)を省く事ができる。 しかし、全てのwhere関数呼び出し部分でこの修正を加えるのは非常に骨が折れる。

上記の知見から、修正方針をまとめると下記の2点となる。

  • whereには、呼び出し時に注目していない(=ムダな)フィールドの存在をチェックするコードを書きたくない
  • whereの呼び出し部分で、whereを必要なフィールドのみチェックする関数に手作業で置き換えるのは大変なのでやりたくない

これを実現するために、マクロを使用する。マクロを使用すれば、必要なフィールドのみチェックするコードを持つwhere関数を生成することが可能となる。

例えば、下記のようなbackwordsというマクロを定義すれば、Lispの逆表記バージョンの言語を作ることも可能となる。 このように、マクロを用いることで、REPL評価前の式を変形させることができる。

> (defmacro backwords (expr) (reverse expr))
> (backwords ("hello, world" t format))
hello, world
NIL

このようにマクロを使用して、参照されるそれぞれのフィールドに対応した式を返すwhereマクロを定義してみる。
ここからは、下記の手順でwhereの改良版を実現する。

  1. make-comparison-expr関数:フィールドに対応する比較式を作る
  2. make-comparison-list関数:make-comparison-expr関数を使って、複数のフィールドに対応する比較式を作る
  3. whereマクロ:make-comparison-list関数を使って、複数のフィールドに対応する比較式をANDで包んだ関数を返す

まず、引数に指定したフィールドに対応した比較の式を返すmake-comparison-expr関数を定義する。

> (defun make-comparison-expr (field value)
    "指定したフィールドに対応する比較式を返す
     field: フィールド名
     value: 値
     ret: フィールドに対応する比較式"
    `(equal (getf cd ,field) ,value))

これを使用すると、下記のようになる。

> (make-comparison-expr :rating 10)
(EQUAL (GETF CD :RATING) 10)
> (make-comparison-expr :title "Give Us a Break")
(EQUAL (GETF CD :TITLE) "Give Us a Break")

次に、make-comparison-expr関数が返す比較式を集めるmake-comparison-list関数を定義する。

(defun make-comparison-list (fields)
  "複数の指定したフィールドに対応する比較式のリストを返す
   fields: フィールドのキーワードと値が交互に並んだリスト
   ret: フィールドに対応する比較式のリスト"
  (loop while fields
    collecting (make-comparison-expr (pop fields) (pop fields))))

最後に、make-comparison-list関数が返す比較式のリストをANDで包むwhereマクロを定義する。

(defmacro where (&rest clauses)
  ;; 複数のフィールドに対応する比較式を`AND`で包んだ関数を返す
  ;; clauses: whereで指定するフィールド(複数)
  ;; ret: 指定された複数フィールドに対応するフィルタ関数
  `#'(lambda (cd)
       (and ,@(make-comparisons-list clauses))))

NOTE
&restが引数リストにあると、関数やマクロは任意個の引数をとれるようになり、その引数はリストとして&restの直後にある仮引数を束縛する。
つまり、下記のようになる。

> (defun f (&rest args)
    args)
> (f :a 1 :b 2 :c 3 :d 4)
(:a 1 :b 2 :c 3 :d 4)

NOTE
,@は、評価された結果のリストを展開する。つまり、下記のようになる。

;; ,@を使わない例
`(and ,(list 1 2 3)) => (AND (1 2 3))

;; ,@を使う例
`(and ,@(list 1 2 3)) => (AND 1 2 3)
`(and ,@(list 1 2 3) 4) => (AND 1 2 3 4)

whereマクロを呼び出すとどんなコードが生成されるのかを知るには、macroexpand-1関数が使用できる。

> (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

実際に、上記で定義したwhereマクロを試してみる。

> (select (where :title "Give Us a Break" :ripped t))
((:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T))

このように、whereマクロは最初のwhere関数よりも短く簡潔で、かつ、特定のフィールドと結びついていないため、フィールドの構成変更に影響されない。

3.9 まとめ

上記では、コードの重複を取り除こうとしたら、同時にコードの汎用性も向上した。何故だろうか?
なぜなら、マクロはシンタックスレベルでの抽象化を施す仕組みの1つであり、抽象化というのは物事の普遍性を表現する手法の1つであるからである。

なお、この簡易データベースのコードにおいて、CDとそのフィールド固有のコードを含む箇所は、make-cdprompt-for-cdadd-cdのみとなっていることにも注目するべきである。

4.1 なんでこんなに括弧があるの?

括弧がたくさんあるのは、M式よりもS式の方が初期のLisperに好まれたからである。

4.2 ブラックボックスをばらして中を見ると

Lispのシンタックスとセマンティクスがどのように定義されるのかを見る。

たいていの言語処理系は、ブラックボックスの中で、プログラムのテキストから動作やオブジェクトコードへと変換する作業を、それぞれ一部分ずつ担うサブシステムに分割されている。
よくあるのは、処理系を3つのフェーズに分割し、それぞれの出力結果を次のフェーズに流し込む方式である。

たいていの言語処理系の処理の流れ

文字列
 -> [字句解析器] 文字列を字句に分解する
 -> 字句
 -> [パーサ] 言語の文法に基づいてプログラムに置ける式を表す木を作る
 -> 抽象構文木(AST: abstract syntax tree)
 -> [評価器] インタプリタとして直接実行したり、機械語のような他の言語へとコンパイルする

Common Lispの場合、この処理のフェーズ分割が少し異なる。

文字列
 -> [読み取り器(reader)] 文字列をS式と呼ばれるLispオブジェクトに変換する
 -> Lispオブジェクト(S式)
 -> [評価器(evaluator)] セマンティクスを評価して、インタプリタとして直接実行したり、機械語のような他の言語へとコンパイルする

4.3 S式

リストとアトム

S式の基本構成要素は、 リストアトム である。

  • リスト:括弧で括られており、単数、または複数のS式を要素に持つ
  • アトム:リスト以外のS式

LispのBNF(バッカス・ナウア記法)を参照すると、このあたりの詳細が分かる。

シンボル

先述のformat*bd*といった名前は、シンボルと呼ばれるオブジェクトによって表されている。
読み取り器は、指定された名前が変数名なのか関数名なのかは一切知らない。
名前には、下記以外、ほとんどの文字を使用することができる。

  • 数字(シンボル名の全てが数字でなければ使用可能)
  • 開き括弧 (
  • 閉じ括弧 )
  • ダブルクォート "
  • シングルクォート '
  • バッククォート ```
  • カンマ ,
  • コロン :
  • セミコロン ;
  • バックスラッシュ \
  • 縦線 |

読み取り器が名前をシンボルオブジェクトに変換する方法について、下記の2点を理解すべきである。

  1. 名前に出てくる大文字と小文字を読み取り器が扱う方法
  2. 同一の名前をいつも同一のシンボルとして読むのを読み取り器が保証する方法

1. 名前に出てくる大文字と小文字を読み取り器が扱う方法

読み取り器は、名前を読む時に、エスケープされていない文字を全て大文字に変換する。
つまり、読み取り器は、fooFOOFooも、全て同じシンボルとして読む。
そして、\f\o\o|foo|は、エスケープされているため、fooというシンボルとして読まれる。
現代の標準的なLispでは、コードを全て小文字で書き、読み取り機で名前を大文字に変換する。

2. 同一の名前をいつも同一のシンボルとして読むのを読み取り器が保証する方法

同一の名前をいつも同一のシンボルとして読むのを読み取り器が保証するために、読み取り器はシンボルを インターン(intern) する。
このインターンにより、S式のどこに同じ名前が現れてもそれを表すために同一のオブジェクトが使用されることになる。

インターンの仕組み

  1. 読み取り器は、名前を読み込んで大文字に変換したら、その名前のシンボルが パッケージ(package) と呼ばれる表に存在しているかどうか調べる。
  2. シンボルが表に存在していたら、そのシンボルを返す。
  3. 見つからなかったら、新しいシンボルを作成して表に追加し、それを返す。

4.4 LispフォームとしてのS式

ここからは、評価器について説明する。

読み取り器がテキストの集まりをS式に変換したら、そのS式はLispコードとして評価できる。 しかしながら、S式の全てがLispコードとして評価できるわけではない。 すなわち、S式がLispコードのセマンティクスを満たすかどうかを評価する必要がある。

評価器は、S式がLispコードとして評価できるかを決める評価規則を持つ。
といっても、この評価規則は非常にシンプルである。 すなわち、「正規のLispフォームとは、任意のアトム、または、リストの先頭要素がシンボルとなっているリスト」という規則である。

アトムには2種類ある。シンボルと、シンボル以外である。

  • シンボル: 変数の名前とみなされ、その変数の現在の値として評価される(定数もこちら)
  • シンボル以外: シンボル意外である以外の 自己評価型(self-evaluating) のオブジェクト(数値、文字列)

NOTE
自己評価型とは、評価機構に渡したら自分自身を評価値として返すという意味である。

シンボルも、変数名にシンボル自身を代入すれば自己評価型になる。
例えば、標準で用意されている真偽値であるtnilがそれにあたる。
他にも、 キーワードシンボル も自己評価型のシンボルである。 キーワードシンボルとは、シンボル名の先頭に:がついたものである。 読み取り器は、:から始まる名前をインターンするとき、そのシンボルを値として持つ定数を自動的に定義する。


正規のリストフォームは、必ず先頭要素がシンボルである。
そして、リストが評価される方法には、このシンボルの種類によって3種類に分けられる。

  • シンボルが関数の名前、または、未定義の名前:関数呼び出し
  • シンボルが特殊オペレータの名前:特殊オペレータ
  • シンボルがマクロの名前:マクロ

4.5 関数呼び出し

関数呼び出しのルールは、「リストの先頭以外の要素を評価し、その結果の値を関数に渡す」である。
したがって、関数呼び出しフォームのシンタックスにおいては、「リストの先頭以外の要素は well-formed である必要がある」というルールも自明的に存在する。

関数呼び出しのシンタックス

(function-name arguments*)

4.6 特殊オペレータ

関数で定義できないような操作は、特殊オペレータによって実現されている。
例えば、ifのように、先頭以外の要素について、片方を評価しない場合があるような操作は関数では定義できない。
なぜなら、関数では、先頭以外の要素について最初に全て評価しきってしまうからである。

例:ifのシンタックス

(if test-form
    then-form
    [else-form])

また、他のフォームが評価される環境を操作するような特殊オペレータも存在する。 つまり、letなどがそれにあたる。

4.7 マクロ

マクロとは、端的に言えば、シンタックスを拡張する手段をユーザに提供する機能である。
マクロは、S式を引数として受け取り、そのS式(マクロフォーム)の代わりに評価される別のS式(Lispフォーム)を返す関数である。

マクロフォームの評価は2段階である。

  1. マクロフォームの要素がそのまま(=通常の手順で評価器に評価される前に)マクロ関数に渡される
  2. マクロ関数が返すLispフォーム(=マクロ展開されたS式)が、通常の評価手順で評価される

したがって、例えばcompile-fileコマンドでコードをコンパイルする時は、下記の手順でマクロが展開される。

  1. ファイル中の全てのマクロフォームが、関数と特殊フォームだけになるまで再帰的にマクロ展開される
  2. マクロのない状態のコードが、loadコマンドで読み込める形式のfaslファイルへとコンパイルされる
  3. コンパイルされたコードは、ロードされるまで実行されない

ここで、重要な事が2点分かる。

  • マクロ展開されるのはコンパイル時なので、ファイルのロード時や、ファイル中に定義された関数の呼び出し時にマクロ展開のコストはかからない
  • 評価器は、S式をマクロ関数に渡す前に評価しないため、マクロフォーム内のS式は、それぞれのマクロが解釈できる形で自由に記述可能である

4.8 真偽、等しさ

真偽

Lispの真偽は非常にシンプルである。
すなわち、nilが偽で、それ以外は全て真である。
ただし、明確に真偽値の真であることが分かるように、tが特別に用意されている。

nil'nil()'()は、全て同じものとして評価されるように設計されているため、これらは全て偽である。
同様に、t'tも、全て同じものとして評価されるように設計されているためこれらは全て真である。

NOTE
上記のシンボルのうち、クォート有りのものは、特殊オペレータquoteによって、与えられたシンボルそのものとして評価され、クォート無しのものは、値としてシンボルnilやシンボルtを持つ定数への参照として評価される。
これにより、実質的にはnilが偽でそれ以外が真である、といえる。

等しさ(同一性、同値性)

eq

eqは「オブジェクト同一性」を比較する。
すなわち、2つのオブジェクトが同一であるとき、それらはeqである、と換言できる。

ただし、同じ値を持つ2つの数値や2つの文字を同一であるとするかは 処理系依存 である。 そのため、数値や文字に対して同一性を比較したい時にeqを使ってはならない。

eql

数値や文字の「値が等しい」、つまり、同値であることを比較したい場合は、eqlを使用する。
ただし、eqlは、同じ数値や文字の値を表している 同じクラス のオブジェクトを等しいとみなす。 したがって、(eql 1 1)は真であるが、(eql 1 1.0)はオブジェクトのクラスが違うため偽である。

equal

equalは、同じ構造と内容を持つリストを再帰的に等しいとみなす。
また、同じ文字で構成された文字列もequalでは等しいと判断される。
さらに、一次元配列やパス名といったデータ型に対しても、equaleqlよりもゆるい判断をする。
それ以外のデータ型については、eqlを用いたときと同じ判断をする。

equalp

equalpは、equalよりも判断基準がさらに緩く、文字列の比較において、大文字小文字の区別をしない。
さらに、数値に対しては、数学的に同じ値であれば等しいと判断する。
したがって、(equalp 1 1.0)は真となる。
また、equalpで真となる要素を持つリスト同士も、equalpで真となる。
同様に、equalpで真となる要素を持つ配列同士も、equalpで真となる。
その他の大抵のデータ型については、eqlを用いたときと同じ判断をする。

4.9 Lispコードの書式付け

インデント

殊、Lispにおいては、コードのインデントには気を遣うべきである。
しかし幸いなことに、SLIMEやSLIMVのようなエディタの機能を使用すれば、自動的にインデントを適切に修正してくれる。
重要なのは、どのようなインデントが一般的であるのかを覚えておくことである。

コメント

Lispのコメントの流儀として一般的なものを下記に示す。

;;;; aaaaaa セミコロン4つ・・・ファイルヘッダのコメント

;;; bbbbbbb セミコロン3つ・・・以降のコードセクションに対する段落コメント
;;; bbbbbbb

(defun (foo)
  (let ((x 1))
    ;; cccc セミコロン2つ・・・以降のコードに対するコメント
    (some-function-call)
    (another-function-call x) ; dddd セミコロン1つ・・・この行のみに対するコメント
    (another)))

5.1 新しい関数の定義

(defun name (parameter*)
  "省略可能なドキュメンテーション文字列"
  body-form*)

5.2 関数のパラメータリスト

parameterの部分は、関数のパラメータリストと呼ばれる。
パラメータリストの指定方法は複数ある。
次の章で、それらについて説明する。

5.3 オプショナルパラメータ

必須のパラメータ以外に、任意で指定したいパラメータの前に&optionalを指定する。
全ての必須パラメータに値が渡された後、まだ引数が残っていたら、その値がオプショナルパラメータに割り当てられる。
引数がオプショナルパラメータより先に足りなくなったら、残りのオプショナルパラメータにはNILの値が割り当てられる。

(defun foo (a b &optional c d)
  (list a b c d))

(foo 1 2)
; => (1 2 NIL NIL)
(foo 1 2 3)
; => (1 2 3 NIL)
(foo 1 2 3 4)
; => (1 2 3 4)

オプショナル引数の値が、呼び出し元により指定されたNILなのか、それともデフォルトのNILなのかを知るには、下記の通りにする。

(defun foo (a b &optional (c 3 c-supplied-p))
  (list a b c c-supplied-p))

(foo 1 2)
; => (1 2 3 NIL)
(foo 1 2 3)
; => (1 2 3 T)
(foo 1 2 4)
; => (1 2 4 T)

まとめると、オプショナルパラメータの構文は下記の通りになる。

  • 必須パラメータの後に&optionalシンボルをつけることで、続く引数がオプショナルパラメータ扱いになる。
  • 必須パラメータには、デフォルト値を持たせることができる。
  • 必須パラメータには、その引数に値が渡されたかを判定するためのフラグを割り当てることができる。
(defun foo (必須パラメータ1 必須パラメータ2 &optional オプショナル1 (オプショナル2 デフォルト値 引数指定判定フラグ)))

5.4 レストパラメータ

可変個のパラメータを扱うための仕組みとして、レストパラメータがある。
関数の引数に&restシンボルがあったら、それ以降のパラメータを全てまとめて扱うことができる。
つまり、&restパラメータが含まれていたら、必須パラメータとオプショナルパラメータに割り当てられた残りの引数はリストにまとめられ、そのリストが&restパラメータの値になる。

(defun + (&rest numbers)
  "0個以上の数値を加算する
   numbers: 数値(0..*)"
  ...)
(defun format (stream string &rest values)
  "文字列を整形して出力する
   stream: 出力ストリーム
   string: 出力対象文字列
   values: フォーマット対象の値"
  ...)

まとめると、レストパラメータの構文は下記の通りになる。

  • 必須パラメータとオプショナルパラメータの後に&restシンボルをつけることで、続く引数がレストパラメータ扱いになる。
  • 必須パラメータに渡された値は、リストとなり、必須パラメータに割り当てられる。
(defun foo (必須パラメータ1 必須パラメータ2 &rest レストパラメータ1 レストパラメータ2))

5.5 キーワードパラメータ

例えば、ある関数に4つの引数を持たせたいとする。
その関数が呼び出されるとき、ほとんどの呼び出し元は4つのパラメータのうち1つしか渡さず、しかもどのパラメータも偏りなく呼び出されるとする。 そのような場合、キーワードパラメータを使用するとうまく解決できる。
下記の通り、キーワードパラメータを使えば、関数呼び出しの際の引数の指定の順番によらない処理が可能となる。

(defun foo (&key a b c)
  "キーワードパラメータで渡された引数のリストを返す"
  (list a b c))

(foo)
; => (NIL NIL NIL)

(foo :a 1)
; => (1 NIL NIL)

(foo :b 1)
; => (NIL 1 NIL)

(foo :c 1)
; => (NIL NIL 1)

(foo :a 1 :c 3)
; => (1 NIL 3)

(foo :a 1 :b 2 :c 1)
; => (1 2 3)

(foo :a 1 :c 3 :b 2)
; => (1 2 3)

既に登場したパラメータを、デフォルト値のフォームとして使用することもできる。

(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
  ...)


(foo :a 1)
; => (1 0 1 NIL)

(foo :b 1)
; => (0 1 1 T)

(foo :b 1 :c 4)
; => (0 1 4 T)

(foo :a 2 :b 1 :c 4)
; => (2 1 4 T)

呼び出し元がパラメータ指定するために使うキーワードを、実際のパラメータと異なるものにする事もできる。
もしAPIなどを実装する場合には、このような機能が役立つ。
つまり、内部的には簡潔な変数名を使用し、外部に公開する引数には説明的なキーワードを使用する。

(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))
  (list a b c c-supplied-p))

(foo :apple 10 :box 20 :charlie 30)
; => (10 20 30 T)

5.6 異なるパラメータの併用

上記の、必須パラメータ、オプショナルパラメータ、レストパラメータ、キーワードパラメータは、全て併用することができる。
ただし、これらには使用して良い順番がある。下記の順番で使用可能である。

  1. 必須パラメータ
  2. オプショナルパラメータ
  3. レストパラメータ
  4. キーワードパラメータ

NOTE &optional&keyの組み合わせは、予想外の結果になるため、併用してはいけない。
というのも、呼び出し元がオプショナルパラメータを全て与えなかった場合、オプショナルパラメータがキーワードパラメータの値を食ってしまうためである。

(defun foo (x &optional y &key z)
  (list x y z))

;; これは正しく動作する
(foo 1 2 :z 3)
; => (1 2 3)

;; これも正しく動作する
(foo 1)
; => (1 NIL NIL)

;; これはエラーとなる
(foo 1 :z 3)
; => ERROR

最後の例では、:zがオプショナルパラメータとして認識されてしまう。 こうなると、残りは引数3のみである。 これをキーワードパラメータとして解釈することができないため、エラーとなる。

レストパラメータとキーワードパラメータの組み合わせは、少々とっつきにくいが、安全に動作する。

(defun foo (&rest rest &key a b c)
  (list a b c))

(foo :a 1 :b 2 :c 3)
; => ((:A 1 :B 2 :C 3) 1 2 3)

上記は、レストパラメータとキーワードパラメータの解釈を素直に理解すれば納得できる。

  • レストパラメータ…&restシンボル以降の引数を全て集めてリストに入れる
  • キーワードパラメータ…&keyシンボル以降の引数を、キーワードの対として集める

5.7 関数の戻り値

関数の戻り値は、その関数を抜ける最後に評価された値となる。 関数の任意の箇所で、直ちに値を返す場合は、特殊オペレータreturn-fromを使用する。
ただし、return-fromは関数には直接関係していない。
これは、blockで作られたブロックから抜けるために使用するものである。

defunは自動的に関数の本文全体を関数と同名のブロックで囲う。
そのため、return-fromを関数の名前で評価することにより、戻したい値と一緒に直ちにその関数から脱出できる。

return-fromには、脱出したいブロックの名前を最初の引数として指定する。
この名前は評価されないため、クォート不要である。

例を下記に示す。
下記の関数は、引数よりも大きな積になる10未満の数のペアを探すためにネストしたループを使用している。 数のペアが見つかったらすぐにreturn-fromを使ってペアを返している。

(defun foo (n)
  (dotimes (i 10)
    (dotimes (j 10)
	 (when (> (* i j) n)
	   (return-from foo (list i j))))))

5.8 データとしての関数または高階関数

@JironBach
Copy link

JironBach commented Mar 12, 2021

こんばんは。「実践CommonLisp」を参考に実装までやってたのですが
26.6 クエリパラメータ

(net.aserve:publish :path "/show-query" :content-type "text/html"
                    :function 'show-query-params)
(defun show-query-params (request entity)
  (with-http-response (request entity :content-type "text/html")
    (with-http-body (request entity)
      (net.html.generator:html
        (:html
        (:title "Query Parameters"
          (if (request-query request)
          (html
            (:table
              (loop for (k . v) in (request-query request)
              do (html (:tr (:td k) (:td v)))))))))))))

のようにしてみたのですが
表示が空白になり、ソースを見ると

<title>Query Parameters
</title>

の表示されてて、テーブルの形にはなってるようなのですが
中身が表示されません。

request-queryのデータの取得の仕方がまずいでしょうか。

エラーメッセージに
「unknown html keyword :NAME」
と出ます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment