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

Practical Common Lisp (12)

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

引き続きPractical Common Lisp 第4章4. Syntax and Semanticsを読んでメモしてみています。

S-expressions As Lisp Forms

  • リーダがテキストをS式に変換して初めて評価器の出番となる。
  • リーダが拾って来たS式、全部を全部評価できるというわけではない。
  • 評価器はLispのフォームを決定する役割があるが、これは非常にシンプルなルールに基いている。

4-どんなアトム(リスト以外のものと空リスト)も正しいLispフォームで、また、シンボルが第一番目の要素になっているリストも正しいフォームとして扱われる。

  • アトムは、シンボルと、それ以外の二種類に分類できる。
  • 評価されるLispフォームとしてのシンボルは現在保持している値となる。
    • "変"数という名前(Variable)にも注意しよう。値が"変"わらない"変"数もあり、"Constant Variable"と呼ばれる。(日本語だと、定数っていうし問題なしじゃなかろうか、どうなんだろう) 例としてはPIがCommon Lispには定数として定義されている。
  • それ以外のアトムは、それ自体が評価された値になるものになる。"Hello, World"を評価すると"Hello, World"
  • シンボルでもそういう例は存在する。Tと、NILを評価してもTとNIL、他には、キーワードシンボルが該当する。":"から開始されているシンボル。:FOOを評価すると、:FOO
  • リストの評価方法について
    • シンボルが第一番目の要素のリストは正しい表現として扱われるが、しかし、そういうリストには三種類あり、それぞれ随分と違った方法で評価される
    • 最初のシンボルの名前が、関数であるか、マクロであるか、スペシャルフォームであるか、の三種
    • 以降、関数呼び出しフォーム、マクロフォーム、スペシャルフォームと呼ぶことにする。

うーん、この節は自分の英語力で読むにはちょっと難しいなー。

2007-12-19

Practical Common Lisp (11)

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

引き続きPractical Common Lisp 第4章4. Syntax and Semanticsを読んでメモしてみています。

S-expressions

  • 基本的なS式は、リストとアトムで構成される
  • リストは、括弧で区切られていて、任意の数の空白で区切られた要素を含むことができる。
  • アトムはそれ以外のすべて
  • セミコロンは、コメント記号で、それ以後改行までコメントとして扱われる
  • ということで、文法的に覚えるべきことは非常に少ない。あとはアトムの種類を覚えることくらいになる。
  • アトムには、数字、文字列、名前(シンボル名)がある。
  • 数字は、そのものずばり、数字で構成されたもの。+と-記号を付けて表記できる。小数点と、有理数の表記のための/、そして、指数表記もつかえる。
123       ; the integer one hundred twenty-three
3/7       ; the ratio three-sevenths
1.0       ; the floating-point number one in default precision
1.0e0     ; another way to write the same floating-point number
1.0d0     ; the floating-point number one in "double" precision
1.0e-4    ; the floating-point equivalent to one-ten-thousandth
+42       ; the integer forty-two
-42       ; the integer negative forty-two
-1/4      ; the ratio negative one-quarter
-2/8      ; another way to write negative one-quarter
246/2     ; another way to write the integer one hundred twenty-three
  • これらの数字は、整数、有理数、浮動小数点数と呼ばれるが、独自の表記方法を持つ、複素数も表現できる。これは10章で解説する。
  • 同じ数字を色々な表現で書くことが可能だということが分かるが、処理系が簡略化するので注意が必要。
  • -2/8や、246/2は、-1/4や、123となる。同様に、1.0と1.0e0は表記の違いということになる。一方、1.0と、1.0d0は違うものとして扱われるが、整数と、浮動小数点数は違うものとして扱われるため。詳細は、10章で。
  • 文字列は以前の章でダブルクオートに囲まれたものとして表現として目にしている。ダブルクオートを中に含めるには、\を直前に付けてエスケープする。エスケープして表現する必要があるのは、ダブルクオート(")と、バックスラッシュ(\)の二つのみ。
"foo"     ; f、o、oを含む文字列
"fo\o"    ; 同上
"fo\\o"   ; f、 o、 \、oで構成された文字列
"fo\"o"   ; f、 o、 "、oで構成された文字列
  • 名前(シンボルの名前という意味?)はLisp内のプログラムの名前として使用している、FORMATや、hello-worldや、*db*など。これらは、シンボルと呼ばれ、処理系は、名前自体が、変数を指しているのか、関数を指しているのか等々は気にしない。
  • 殆どすべての文字がシンボル名に使えるが空白文字は句切りとして使用されているため使えない。数字も使えるが、数字のみで構成されたものは数字自体と区別がつかないので駄目。また、ピリオド(.)も名前につかえるが、ピリオド(.)一文字のみというのは駄目。他に使えない文字というのは10種あり、()'"`,:;\|は使えない。
  • これらの文字を使いたい場合、そのままでは使えないが、\を前に置くか、||で囲むかすれば使える。
  • 重要事項として、リーダは文字の大文字と小文字を区別しないということがあり、全部大文字として扱う。Fooもfoo、FOOも同じ。しかし、\f\o\oと、|foo|は、fooとして扱われ、FOOとは違ったオブジェクトとして扱われる。REPLでfooという名前を打ち込んで、FOOと返ってくるのはこのため。
  • 今日の一般的なコーディングスタイルでは、全部小文字で書くが、結局のところリーダは全部大文字として解釈している。
  • 同じ表記のは常に同じシンボルとして扱われる。
  • リーダは、文字列を全部大文字に直し、シンボルとして取り込もうとする。その際にパッケージの名前一覧に同じ名前がないかをチェックする。見付からなかった場合、新しく一覧に登録される。見付かった場合は、先に登録されていたオブジェクトが使い回される。同じ名前ならばどこでも同一のオブジェクト使われているということでもある。
  • Lispは、Algol系の言語より多くの文字種が使えるということもあるが、ハイフン(-)で区切られた名前(例hello-world)が良く使われているところが特徴的。
  • また、*が前後に付いた変数名は、大域変数を表わすという習慣がある。同じく定数表記は、+を前後に付けるのが慣用表現。内部的なものは%で開始されることが多い、%%から開始というのも同様。
  • Common Lispの標準関数/変数名には、(A-Z)に加え、*、+、-、/、1、2、<、=、>、&、が使われている。
  • 文法に占める比重とは、数字、文字列、シンボルが多いが、他には、ベクタ、単体の文字、アレイ表現がある。これらは、10、11章で扱う。
  • 要点としては、これらをどう組合されてS式が構成されているかということだが、簡単な図を示す。
x             ; Xというシンボル
()            ; 内容が空のリスト
(1 2 3)       ; 3つの数字を含むリスト
("foo" "bar") ; 2つの文字列を含むリスト
(x y z)       ; 3つのシンボルを含むリスト
(x 1 "foo")   ; 数字とシンボルと文字列を含むリスト
(+ (* 2 3) 4) ; シンボルと数字を含むリストと数字を含むリスト
  • もう少し複雑な例としては、全体として4つの要素があるリストで、2つはシンボル、1つは空リスト、1つは、シンボルと文字列を含むリストということになる。
(defun hello-world ()
  (format t "hello, world"))

2007-12-18

Practical Common Lisp (10)

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

引き続きPractical Common Lisp 第4章4. Syntax and Semanticsを読んでメモしてみています。文字ばっかりだと、英語が苦手な自分には何だか良く分かりません…。とりあえず、ウェブ翻訳で訳したりして適当に妄想してるメモになっております。

Breaking Open the Black Box

  • Lispの文法と意味論の詳細に立ち入る前に他の言語とはどういったところが違うのかを考察してみる。
  • 殆どの言語の処理系(コンパイラ、インタプリタ共に)はブラックボックスのようなものになっている。ユーザはテキストをそれに読み込ませて結果を受けとる。
  • ブラックボックスの詳細に立ち入るならば、もちろん沢山のパートに分けられる。字句解析や用いられるデータ構造等様々あるが、ブラックボックスたる所以は、その構造や機構が専らブラックボックス内部の処理プロセスで用いられるのみで、それ自体は、処理系製作者向けのものといった感じになっているところ。
  • Common Lispはちょっと違っていて、どう処理系を実装するかと、どう言語が定義されるかの二つが考慮された帰結として、一つのブラックボックスで処理されるのではなくて、二つのブラックボックスで二段階に処理される。テキストをLispのオブジェクトに変換するリーダが最初のもので、それを評価する評価器がそれに続く。
  • リーダは文字列をLispオブジェクトたるS式への変換を担当する。Lisp以外の言語でいうと処理系内部での構文木の生成にあたる。
  • 評価器は構築されたS式からLispの文法を定める。S式が全部正しいLispオブジェクトという訳ではない、例えば、(foo 1 2)、("foo" 1 2)は共にS式としては正しいが、Lispのフォームとしては、最初が文字列のリストはLispフォームとして意味を成していない。
  • ということで、リーダがどのようにS式を組み立てるか、と、評価器がどのように式を評価するのかの二つに焦点を当てて考察したい。

2007-12-17

Practical Common Lisp (9)

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

やっとPractical Common Lisp 第4章4. Syntax and Semanticsに突入

この章もコードは少なく殆ど説明の様なので、箇条書に纏めることにしました。主に文法を説明する章のようです。

What's with All the Parentheses?

  • Lispの文法はAlgol系のものとは大分違う。
  • 括弧で囲まれた前置記法というのが特徴的。この記法については好き嫌いが分かれる。
  • McCarthyが最初にLispを発明した時、M式というAlgol風の記法も考えていて、実装しようと思っていたが、結局実装されず仕舞いだった。このことは、"Historical of Lisp"に詳しい。
  • コーディングには主にM式を使い、それが機械的にS式に変換され、それをマシンが実行するという流れを考えていたが、プログラマははS式を好んで使いはじめてしまい、M式が必要に迫られることはなかった。
  • 45年前のユーザが、S式を好み、また、それが言語をより強力にするのだということをみつけていたということでもある

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は、条件を満さないもののリストを返す。つまり、リストから不要なものを取り除いた結果を返す。

2007-12-14

Practical Common Lisp (6)

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

引き続きPractical Common Lisp 第三章3. Practical: A Simple Databaseを読んでみています。一日1段落のペースになりつつあります…。

Querying the Database

とりあえず出てきたコードを暗記して再現して感想をメモ
(select :artist "Dixie Chicks")

とりあえず、こういう関数を作って検索することにしたのだろう。

(defun select-by-artist (artist)
  (remove-if-not (lambda (cd) (equal (getf cd :artist) artist))
		 *db*))

できた。アーティスト名を引数にとって検索するものの様子。

Practical Common Lispでは、lambdaにはfunction(#')を付ける派のようです。

(defun select (selector-fn)
  (remove-if-not selector-fn *db*))

できた。これはなんだろう高階関数化するんだろうか。

(defun artist-selector (artist)
  (lambda (cd) (equal (getf cd :artist) artist)))

無名関数に名前を付けるらしい。

(select (artist-selector "Aldo Ciccolini"))

のように使うらしい。でもなんとなく無理矢理な気が。

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

間違えた。動きは同じだけど、andと、デフォルト値tの組み合わせで

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

だった。

自分には、

(or (if pred t nil)
    (if pred t nil))

の方が

(and (if pred nil t)
     (if pred nil t))

より自然に感じるな。

とりあえず、一般化されたwhereという関数を作って、selectに渡すらしい。関数型言語っぽい。

以上を踏まえて本文を読んでみる

(select :artist "Dixie Chicks")という感じで検索できるようにしたい。

  • REMOVE-IF-NOTの説明
    • リストから条件に合わないものを削除して、結果のリストを返す。結果として欲しいもののリストが返る。
    • REMOVE-IF-NOTには関数を渡す。LAMBDAで作った無名関数も渡せる。
  • LAMBDAの説明。
    • 無名関数を作る。名前がないことを除いては、DEFUNの様なもの。
  • 以上を踏まえて、select-by-artistを作ってみる。
  • 同じ流れでselect-by-title, select-by-rating, select-by-title-and-artistと作っても良いが、もう少し一般化して、selectという関数を引数に取るより一般化された関数を作ってみる。
  • selectの中の変数名に#'は付けなくて良いのかという疑問→いりません
  • selectに渡す関数をもっと一般化してみる
    • キーワードパラメータの説明
(defun foo (&key a b c) (list a b c))

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

という風に、キーワードと値のペアを便利に渡す仕組みがCommon Lispにはあるので、これを利用する。

      • 特徴
        • キーワード:値というペアで値が特定できるので、関数呼び出しの時に、引数のペアの順番は任意にできる。
        • 関数呼び出しの時に、引数のペアを与えなければ、定義時に設定したデフォルト値が渡される
        • デフォルト値設定について
(defun foo (&key a (b 20) (c 30 c-p)) (list a b c c-p))

という風に指定できる。

aは指定していないが、指定していない場合、デフォルト値としてNILが設定される。

bは、デフォルト値は20

cは、デフォルト値は30で、更に、値が指定されているかどうかを判定する機構も使っている。

この場合、c-pという変数に、tかnilが渡る。

  • 以上を踏まえて、SQLっぽく、whereという関数を定義することによって、
(select (where :artist "Dixie Chicks"))

という風に使えるようにしてみる。

  • where関数の定義と説明
    • ifの中で、ripped-pを判定しているのは、
(if ripped (equal (getf cd :ripped) ripped) t)

では、rippedの真偽を確かめたいのか、それとも引数の指定がないのでデフォルト値でnilが渡ってきたのが区別できないため。ripped-pで調べる必要がある。

2007-12-13

CLOSチュートリアル (7)

| 17:26 | CLOSチュートリアル (7) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (7) - わだばLisperになる

Common Lisp クックブックさんのところのCLOSのチュートリアルに新しい練習問題(CLOSチュートリアル - 4.6. 修飾子とメソッド結合)が出たので挑戦!。

問題.1

initialize-instance に :after メソッドを追加し、すべての aardvark をイギリスのケンブリッジ(Cambridge, England)から来たことにします。もう一つ、次の関係が成り立たなくなるようなメソッド(どんな修飾子をつけますか?)を追加しなさい。

解答.1

とりあえず、initialize-instanceと:afterの組み合わせで作成してみました。

(defmethod initialize-instance :after ((aardvark aardvark) &rest init-args)
  (declare (ignore init-args))
  (setf (slot-value aardvark 'comes-from) "Cambridge, England"))

animalクラスでcomes-fromは:readに指定されているので、(setf (comes-from aardvark) "Cambridge, England")はできないので、スロット変数を直に指定してちょっと強引に書き換え。これで良いんだろうか。もっと良い方法があるような…。

:accessorだったなら

(defmethod initialize-instance :after ((aardvark aardvark) &rest init-args)
  (declare (ignore init-args))
  (setf (comes-from aardvark) "Cambridge, England"))

でいけます。

;; 動作
(setf zot (make-instance 'aardvark :legs 4 :comes-from "Brittany"))

(comes-from zot)
;=> "Cambridge, England"

それで次は

(make-instance 'cannibal :diet (make-instance 'cannibal))

が成立しないようなメソッドということですが、とりあえず、cannibalクラスを定義して、

(defclass cannibal (mammal) ())

(defmethod initialize-instance :before ((cannibal cannibal) &rest init-args)
  (when (eq (class-of cannibal) (class-of (getf init-args :diet)))
    (error "Cannibal!, Cannibal!")))

と:beforeを使ってdiet引数に同じクラスが渡された場合にerrorになるようにメソッドを追加してみました。しかし、これで良いんだろうか。

;; 動作
(setq cannibal (make-instance 'cannibal :diet (make-instance 'cannibal)))
;=> エラー "Cannibal, Cannibal"

(setq cannibal (make-instance 'cannibal :diet "foo"))
;=> #<CANNIBAL #x30004393E88D>

問題.2

initialize-instance は、他のオブジェクトシステムのコンストラクタを強化したものと見ることもできます。ただし、CLOSにはデストラクタがありません。デストラクタがないのは問題ですか?

解答.2

まず、デストラクタがなんだか良く分からないので、とりあえず、Wikipediaで調べてみたところ、コンストラクタの逆で後始末をするものということで、「Javaではデストラクタは存在せず、近い機能に自動ガベージコレクションによって機能するファイナライザがある。」ってことなので、Common LispにもGCあることだし、GC任せってことでしょうか。

Practical Common Lisp (5)

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

引き続きPractical Common Lisp 3. Practical: A Simple Databaseを読んでみています。どんどんペースが落ちてきました…。

Saving and Loading the Database

とりあえず出てきたコードを写経(暗記して再現)して感想をメモ
(defun save-db (filename)
  (with-open-file (output filename
			  :direction :output
			  :if-exists :supersede)
    (with-standard-io-syntax 
      (print *db* output))))

できた。これで*db*の内容をファイルに書き出すんだろう。

with-standard-io-syntaxってのは初めて使った。

CLHSで確認してみたところ、これに囲まれた入出力は標準化される(CLHSに一覧あり)ので、処理系の違いを気にする必要がないとのこと。

出力されたデータは、処理系に依存しないものにしたいので、こうしているのだろう。

  • with-standard-io-syntaxを試してみる。
(let ((*print-case* :downcase))
  (print 'foo))
;=>fooと印字される

(let ((*print-case* :downcase))
  (with-standard-io-syntax
    (print 'foo)))
;=>FOOと印字される(標準では:upcaseなため)
(defun load-db (filename)
  (with-open-file (in filename :direction :input)
    (with-standard-io-syntax
      (setf *db* (read in)))))

できたけど、:direction :inputは不要だった。デフォルトではinputになるからだろう。

以上を踏まえて本文を読んでみる。
  • save-db
    • WITH-OPEN-FILEの解説
      • ファイルを開いてストリームと結びつけ後始末もしてくれるマクロ。キーワード引数が色々取れて、入出力の向き、ファイルが存在しない場合の動作等、色々指定可能。
    • PRINTの説明。
      • FORMATと違って、READで読み込める形式で出力することになっている。
    • WITH-STANDARD-IO-SYNTAXの説明。
      • 写経時の考察で大体合っていて、可搬性を高めるためのに使用している。
  • load-db
    • WITH-OPEN-FILEはデフォルトでは:inputを指定した動作となるので省略可能。
    • SETFの説明。
      • *db*の内容を上書きしている。

2007-12-11

Practical Common Lisp (4)

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

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

Improving the User Interaction

とりあえず出てきたコードを写経して感想をメモ
(defun prompt-read (prompt)
  (format *query-io* "~A:" prompt)
  (force-output *query-io*)
  (read-line *query-io*))

できた。

*query-io*は初めて使った。

promptに好きな文字列を指定できるらしい。

多分、これでユーザからの問い合わせるのだろう。

(defun prompt-for-cd ()
  (make-cd
   (prompt-read "Title")
   (prompt-read "Artist")
   (prompt-read "Rating")
   (prompt-read "Ripped [Y/N]")))

できた、make-cdの対話的なバージョンの様子。

でもこれだと、rippedがy/nの入力ってことになるが。後で内部的にt/nilに変更するのだろうか。

(defun prompt-for-cd ()
  (make-cd
   (prompt-read "Title")
   (prompt-read "Artist")
   (or (parse-integer (prompt-read "Rating") :junk-allowed 'T) 0)
   (y-or-n-p "Ripped:")))

できた。y-or-n-pを使うことにしたらしい。これだと、y/nを、t/nilに変換できる。

それと、レイティングの数字は、そのままだと、文字列で取得することになるので、parse-integerでパーズしているのだろう。

それで、:junk-allowedをtにすることにより、不正な入力の場合でもエラーにしないで、nilを返すようにして、orでくるんで、デフォルト値を設定している様子。

しかし、y-or-n-pは、処理系によって微妙に動作が違うみたい。Clozure CL(OpenMCL)だと、前後に改行が入ったりする(printの動作に似た感じ)SBCLならそういうことはない。

(defun add-cds ()
  (loop (add-record (prompt-for-cd))
        (if (not (y-or-n-p "Another?")) (return))))

add-recordを対話的にするためのラッパーの様子。

本文を読む
  • *QUERY-IO*を使った問い合わせ方法の説明。
    • FORCE-OUTPUTの実行が必要な処理系もある。
  • READ-LINE関数の説明。
    • 取得する文字列に改行文字は含まれない。
  • PARSE-INTEGEの説明。
    • 不正な値の入力はデフォルトだとエラーになるが、:JUNK-ALLOWEDでTを指定すると、その場合でもNILを返すという挙動になる。
  • ORによるデフォルト値の設定
    • Perl等の、||や、&&演算子と同じ感覚で、and、orが使える。
  • Y-OR-N-Pの説明。
    • ユーザのy/nの入力をt/nilで返す。
  • LOOPの説明。
    • 簡単な無限ループとしての使い方の説明。脱出には、RETURNを使う。

次回、Saving and Loading the Databaseより再開。

CLOSチュートリアル (6)

| 15:18 | CLOSチュートリアル (6) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (6) - わだばLisperになる

Common Lisp クックブックさんのところのCLOSのチュートリアルに新しい練習問題(CLOSチュートリアル - 4.4. すべてはオブジェクトの中にあるCLOSチュートリアル - 4.5. その他の特定子(まだCLOSオブジェクトは不要です))が出たので挑戦!。

4.4 問題.1

Lispの describe 関数は、処理中に describe-object 総称関数を呼び出します。各処理系は各クラスにこのメソッドを実装する必要があり(ただし、standard-object 以外のクラスへの実装は任意です。また、ユーザは自由にメソッドを追加して構いません)、このメソッドがオブジェクトとストリームの二つの引数を受け取るように実装しています。実装者が describe-object のすべてのメソッドを一つのファイルに書くべきか、複数のファイルに分散して書くべきか議論しなさい(例えば、aardvark で特定化されるメソッドを、クラス定義と他のメソッドと一緒に "aardvark.lisp" ファイルに書くべきかどうか)。で、どんな意見が出ましたか?

4.4 解答.1

他のオジェクト指向言語を触ったことがないので、普通がどういうものなのか良く分からないのですが、分けて書けてしまうということは、分けて書いた方が分かりやすいような使い方もできてしまうということかもしれず、別に全部一つに纏める必要はないんじゃないかと思う。

4.4 問題.2

二番目以降の複数の引数でメソッドを特定化しなければならない例をいくつか考えなさい。

4.4 解答.2

(defmethod concat ((str string) (num number))
  (concatenate 'string str (princ-to-string num)))

(defmethod concat ((num number) (str string))
  (concatenate 'string (princ-to-string num) str))

(defmethod concat ((n1 number) (n2 number))
  (concatenate 'string (princ-to-string n1) (princ-to-string n2)))

(defmethod concat ((s1 string) (s2 string))
  (concatenate 'string s1 s2))

とかどうだろう。ちょっと無理矢理な気もするけれど…。本当は可変長の引数が取れるようにしてみたかったけど、defmethodと&restの組み合わせの定番処理方法がいまいち良く分からず…。

4.5 問題.1

リストのための my-describe メソッドを書きなさい。

4.5 解答.1

(defmethod my-describe ((list list))
  (format t "~S is a list object~:[~;and a null object~]." list (null list)))

listは、nil+consなので、nilの場合に追加メッセージを表示するようにしてみた。

4.5 問題.2

antelope クラスのインスタンス Eric のための print-object メソッドを書きなさい。Eric のクラスを変更しても、そのメソッドはまだ適用可能だと思いますか?

4.5 解答.2

(defmethod print-object ((antelope antelope) stream)
  (print-unreadable-object (antelope stream :type 'T :identity 'T)
    (princ "[Hello, Antelope!]")))

Ericのクラスを変更してメソッドの挙動を確認

(class-of Eric)
;=> #<STANDARD-CLASS ANTELOPE>

(print-object eric t)
;=> #<ANTELOPE [Hello, Antelope!] #x300041D3065D>

(change-class eric 'aardvark)
;#<STANDARD-CLASS AARDVARK>

(print-object eric t)
;=> #<AARDVARK #x300041D3065D>

ということで、クラス変更によりantelopeの特定化からは外れた様子。

2007-12-08

CLOSチュートリアル (5)

| 06:30 | CLOSチュートリアル (5) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (5) - わだばLisperになる

Common Lisp クックブックさんのところのCLOSのチュートリアルに新しい練習問題(CLOSチュートリアル - 4.3. 総称関数と次メソッド)が出たので挑戦!。

問題.1

あなたが使用しているLispの処理系の、class-precedence-list の総称関数を調べなさい。

解答.1

OpenMCL 1.1の場合で確認したところ、class-precedence-listは、総称関数として定義されているものの、cclパッケージの%inited-class-cplを呼び出している定義が一つ。

その%inited-class-cplは、%class-cplが一覧をみつけられたならそれを返し、見付からない場合、ccl:*cpl-classes*一覧に問い合わせのクラスが存在する場合、ccl:compute-cplでリストを計算、存在しない場合、動的にアップデートをかけてその後再度計算。

…という感じですが、こんな答じゃ駄目だろうなと。各処理系のclass-precedence-listの簡単なバージョンを実装してみせるのが一番良い解答なのかなという気がしないでもないのですが、実力的に無理なので、できるようになったら再挑戦したいところ。

問題.2

(comes-from Eric) を評価したとき、スロットのリーダ comes-from はどのクラスから継承されているでしょうか?そのメソッドをオーバーライドし、 antelope が常にアフリカから来ることにしなさい(現実的ではありませんが)。

解答.2

とりあえず、comes-fromはどこからかというのは、

(generic-function-methods #'comes-from)
;=>(#<STANDARD-READER-METHOD COMES-FROM (ANIMAL)>) 

で、animalから来てると思います。

それで、オーバーライドですが、簡単に、

(defmethod comes-from ((antelope antelope))
  "Africa")

という定義で

(comes-from Eric)
;=> Brittany

;; オーバーライドする
(defmethod comes-from ((antelope antelope))
  "Africa")

(comes-from (make-instance 'animal :comes-from "foo"))
;=>foo
(comes-from (make-instance 'mammal :comes-from "foo"))
;=>foo
(comes-from (make-instance 'antelope :comes-from "foo"))
;=> Africa
(comes-from Eric)
;=> Africa

というようにantelopeを継承するものは、"Africa"を返すようになります。

問題.3

call-next-method の無限エクステントの効果を試してみなさい。

解答.3

これはなかなかどの辺が、無限エクステントなのか理解できていないのですが、

(defclass one () ())
(defclass two (one) ())
(defclass three (two) ())

というクラスを定義して

(defmethod baz (obj)
  (print "end"))

(defmethod baz ((one one))
  (print "1"))

(defmethod baz ((two two))
  (print "2"))

(defmethod baz ((three three))
  (print "3")
  #'call-next-method)

というメソッドを定義します。

それで、threeのインスタンスを作成するんですが、threeは、返り値として自身のcall-next-methodを返すので、それを変数に保存します。

(setq baz (baz (make-instance 'three)))
;-> 3

それで、保存した変数にfuncallを適用すると

(funcall baz)
;-> 2

となり、上位メソッドが呼ばれていることが確認できます。

それで、ここにきてthreeクラスの内容を変更して、oneが直接のスーパークラスということに変更します。

(defclass three (one) ())

実行結果を確認してみます。

(funcall (baz (make-instance 'three)))
;-> 3
;-> 1

threeの次にoneのメソッドが呼ばれています。

ここで、さっき保存したものを再度呼び出してみます。

(funcall baz)
;-> 2

という風にもとのままです。

…うーん、これで確かめたことになるんでしょうか。

call-next-method自体が、次のメソッドが何であるかを静的に保存しているってのは確認できてる気はするんですが…。

ちなみに、メソッド自体を消しても

(fmakunbound 'baz)

(baz 'two)
;-> error

(funcall baz)
;->2

呼びだせます。

2007-12-06

Practical Common Lisp (3)

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

引き続きPractical Common Lispをさらってみています。

3. Practical: A Simple Database

この章では、簡単なCDのデータベースを作成してみる。

この本の方針とか説明色々。

CDs and Records

CDのアーティスト名、アルバム名、お気に入り度、mp3にリッピングしたかどうか、の4つを格納する。

4つのデータのまとまりを扱うということであれば、Common Lispには沢山の表現方法がある。リストで表現するとか、CLOSを使うとか。

  • とりあえず例示されているコードを写経してみる
(list 1 2 3)

とりあえず、listを使うことにしたのだろう。

(list :a 1 :b 2 :c 3)

plistだろうか。

(getf (list :a 1 :b 2 :c 3) :c)

plistを軸に4つのデータを管理することにしたみたい。

(defun make-cd (album artist rating ripped)
  (list :album title :artist artist :rating rating :ripped ripped))

間違った。albumじゃなくて、titleだった。アルバムのデータのplistを作る関数の様子。

  • 文章を読んでみてメモする

plistについての解説。

普通のリストだけれど、キー、値、が交互に並べた形式をproperty listとかplistと呼ぶ。

":"から始まってるシンボルの意味の説明。

キーワードシンボルと呼ばれる。

plistを作るのには、普通のlist関数を使う。

plistを操作するのには、getfが使える。

(getf plist key)

で、keyと対の値が取り出せる。

値のセーブについては後で。

ということで、CDのデータをplistにする、make-cd関数を作成。

Filing CDs
  • 例示されているコードを写経してみる
(devar *db*)

とりあえず、DBには、大域変数の*db*を定義してそれを使うことにしたらしい。

(defun add-record (cd) (push cd *db*))

大域変数*db*にの結果をどんどんプッシュすることにしたらしく、その為のadd-record関数。

  • 文章を読んでみてメモする

読んでみる。大体予想したことと同じだった。

Looking at the Database Contents
  • 例示されているコードを写経してみる
(defun dump-db ()
  (dolist (cd *db*)
    (format t "~{~A:~10T~A~%~}~%" cd)))

*db*の内容はそのままでは、plistの羅列なので、見易くプリントしてくれる関数らしい。

フォーマット指示子の~Aとか、~Tとかはこの本では小文字にするスタイルらしい。ちなみに、Norvig & Pitmanの

Tutorial on Good Lisp Programming Style(PostScriptファイル)(okshiraiさん訳)では区別しやすいように大文字にすることを推奨。

(defun dump-db ()
  (format t "~{~{~A:~10T~A~%~}~%~}" *db*))

さらにformatで凝ったことをして、dolistのループを不要にした様子。

  • 文章を読んでみてメモする

*db*の内容を見易く表示する関数を作ろう。

dolistの説明。リストの内容を一つずつ処理させたいときに使う。

formatの説明。謎のフォーマット文字列。Cのprintfとか、Perlとかに比べてもそんなに複雑ではない筈。

formatは2つの引数を取って、最初のtは、*standard-output*を指定するという意味。

formatの書式指定子は、~から始まる。printfだと、% 。

~a指示子は、aesthetic(美的)な指示子。(語呂合せで覚えようというのだろうか。)

人が読み易い形で表示してくれる。

引数に文字列を与えると、ダブルクオートを取り除いて表示する。

キーワードシンボルを与えると、:を取り除いて表示する。

ふと思って確認してみた。
:fooというのは、keyword:fooの略記になるわけですが、こういうパッケージを含んだ記法を与えるとどうなるのか、
結果:
(format t "~A" keyword::foo)
(format t "~A" keyword::foo)
(format t "~A" 'keyword::foo)
(format t "~A" 'keyword:foo)
(format t "~A" 'cl-user::foo)
=>FOO
全部FOOでした。

~tというのは、表の整形に使うもので、~10tは、10カラム目からスタートするということ。その前は空白で埋める。

~{と~}の組み合わせは、引数にリストを取り、リストの中身を取り出して~{と~}の間に記述されたパターンにマッチさせながら繰り返し表示する。

~%は改行。

ということで、dolistも省略できたりする。

長いので次回Improving the User Interactionより再開。

2007-12-05

CLOSチュートリアル (4)

| 15:48 | CLOSチュートリアル (4) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (4) - わだばLisperになる

Common Lisp クックブックさんのところのCLOSのチュートリアルに新しい練習問題(CLOSチュートリアル - 4.2. defmethod マクロ)が出たので挑戦!。

問題.1

aardvark と antelope の表示を、デフォルトのものよりもわかりやすくしなさい。すべてのCLOSオブジェクトは print-object メソッドによって表示されます。このメソッドの引数は (オブジェクト ストリーム) です。また、デフォルトのメソッド(standard-object を表示する)がどう定義されているのか考えなさい。

解答.1

ちょっと飾りをつけるということで、aardvarkは、継承関係の一覧を追加で表示し、antelopeはスロットの一覧を追加で表示させることにしてみました。スロットの一覧取得には、Meta!の、moptilitiesパッケージを使ってみています。とりあえず、どっちも総称関数にしてクラスを特定できない場合は、print-objectに処理を丸投げしてます。

(require :moptilities)

(defmethod print-aardvark-object ((obj aardvark) stream)
  (print-unreadable-object (obj stream :type 'T :identity 'T)
    (format stream "(~{~A~^ -> ~})" (mapcar #'class-name 
					  (#+sbcl sb-mop:class-precedence-list
					   #+openmcl ccl:class-precedence-list
					   (class-of obj))))))

(defmethod print-aardvark-object (obj stream)
  (print-object obj stream))

;; 動作
;;==> #<AARDVARK (AARDVARK -> MAMMAL -> ANIMAL -> STANDARD-OBJECT -> T) #x8B7F466>

(defmethod print-antelope-object ((obj antelope) stream)
  (print-unreadable-object (obj stream :type 'T :identity 'T)
    (format stream "[SLOTS:~{~A~^ ~}]" (moptilities:slot-names obj))))

(defmethod print-antelope-object (obj stream)
  (print-object obj stream))

;; 動作
;; ==> #<ANTELOPE [slots:LEGS COMES-FROM DIET] #x8BFF466>

また、print-objectは、総称関数で色々混み入った定義のようですが、簡単に定義するなら

(defmethod my-print-object (obj stream)
  (print-unreadable-object (obj stream :type 'T :identity 'T)))

のように定義できると思います。

問題.2

次のメソッド定義を見て、コンパイラが不要な処理を無視できる可能性についてできるだけ詳しくまとめなさい。(この問題を作るのに Steve Haflich の助けを借りました。)

解答.2

これはちょっと難しくて良く分からないけれど、とりあえず、自分が考えられる範囲で。

1. このメソッドでは、引数bazはfrobクラスと特定されていることになっているため、(mangle)がbaz値のクラスを変更するようなことをしないことを証明でき、かつ、barの結果が、frobクラスに特定されることが証明できれば、etypecaseの判定は無くすことができると思う。

(defmethod foo ((baz frob))
  (loop :initially (mangle)
        :while baz do (setf baz (bar baz))))
さらに、その場合bazがnilになることはない(frobクラスがnilになることはない)ので、
(defmethod foo ((baz frob))
  (loop :initially (mangle)
        :do (setf baz (bar baz))))
にできるんじゃないだろうか。

2. (mangle)が、bazをnilにしてしまう場合。

(defmethod foo ((baz frob))
  (mangle)
  nil)

にできると思う。

…しかし全然この解答には自信がないです。

2007-12-04

Practical Common Lisp (2)

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

ひき続きPractical Common Lispを読んでみます。

2. Lather, Rinse, Repeat: A Tour of the REPL

Common Lispの開発環境についての説明。

Matthew Danish氏とMikel Evins氏が作っている「Lisp in a Box」について。

「Lisp in a Box」というのは、Emacsエディタと、その上でCommon Lisp処理系と連動して動作するSLIMEという強力な開発環境をセットにしたもの。

このセットはCommon Lispの開発スタイルの特徴である、高度に対話的で、インクリメンタルな開発というのを十分に洗練された方法で提供してくれる。

SLIMEはどのOSや、ベースとなるCommon Lispの処理系を選んでも、インターフェースは統一されているので、その違いをあまり意識することなく使うことができる。

商用的なベンダーが提供しているGUIのIDEもあるけど、Common Lispの基本的なことを修得する、という目的には、ちょっとややこしい嫌いがあるので、「Lisp in a Box」がお勧め。

Choosing a Lisp Implementation

とりあえず、ベースとなるCommon Lispの処理系を選ぶ。

処理系の選択というと、Perl, Python, Visual Basic (VB), C#, Javaに馴染み深い人からすると、ちょっと不思議に思うかもしれない。

Common LispはANSIで規格が策定されていて、ANSI Common Lispといえば、それに則って開発された処理系のことになる。

特定のベンダーが提供する処理系が言語の標準実装だったり、特定の実装が唯一の処理系だったりするのとはちょっと違う。

というわけで、ANSI規格に則ったプログラムかを確認していれば他の処理系でも動く。

でも、その一方で、プログラムで処理したいことが全部の規格の範疇に納まってるとは限らない。

そういうプログラムでもできるだけ、移植性の高いものにしたい。

Common Lispでは、そういう処理系の差異を吸収するような柔軟な仕組みもあるので、後の章でそれを解説する。

さて、処理系の選択だけど、メインで使ってるOSが何かってことに一番左右されるんじゃないだろうか。

商用のANSI Common Lisp処理系であるAllegro Common Lispが使いたいって場合には、Linux, Windows, とOS X用が使える。

この本の内容を実行するということであれば試用版Allegro Common Lispが使える。

オープンソースの処理系が良い、ということであれば、選択肢はいくつかあって、SBCLという性能の良い処理系が使える。

SBCLは、CMUのCMU Common Lispをベースに開発されたもの。

CMU Common Lispそれ自体も良い選択だと思う。けれど、SBCLがよりインストールが簡単で、21-bitユニコードをサポートしている。

Mac OSXを使ってるということであれば、OpenMCLが良い選択だと思う。コンパイラはネイティブなマシンコードを出力するし、スレッドもサポートしている。おまけに、Carbonや、Cocoaのインターフェイスも整備されている。OpenMCLには、商用、オープンソースの両方がある。

この本のCommon Lispのコードは、どの処理系を選んでも問題なく動く筈のものを載せている。そうじゃない場合は断り書きを書く。

また、SLIMEは、処理系の違いを吸収してくれる筈。

Common Lispの処理系の出力は、GNU/Linuxで動く、Allegroのものを使った。エラー表示や、デバッガへ入った表示だけど、これは処理系によって結構違う表示になることがある。

Getting Up and Running with Lisp in a Box

始めてのLisp in a Boxのセットアップを最小限の手間で完了させるには、自分が選択したOSやそのOSで使える処理系を良く吟味する必要がある。

これらの選択については、32章で解説している。

Lisp in a Boxでは、Emacsをメインのエディタとして使うが、ちょっとしたEmacsの使い方を知っておく必要がある。

〜Emacsの基本的な使い方についての説明が続く〜

SLIME:C-c C-z
 REPLバッファにカーソルを移動する。

Free Your Mind: Interactive Programming

さて、Lisp in a Boxを起動して最初に目にするものは、

CL-USER>

というプロンプト。

これは、DOSとか、Unixシェルのプロンプトと同じで、入力を受けつけていて、入力→実行というサイクルを繰り返す。

read-eval-print loopとか、略してREPLと呼ばれる。

top-levelとかthe top-level listenerとかthe Lisp listenerとも呼ばれる。

このREPLに式を読み込ませて実行していくことになる。

実際とのところは、エディタ内に読み込まれたファイルの内容を対話的に評価する便利な仕組があるので、いちいちこのREPLにコピペして実行という訳ではない。

Experimenting in the REPL

REPLを使ってみる。まずは、"10"を入力して、Returnを押してみる。

CL-USER> 10
10

REPLという言葉に照らし合せるなら、

CL-USER> 10 ;READ(REPLのR)
10          ;EVAL-PRINT(EしてR)

ってことになる。

"10"はself-evaluatingオブジェクトといってそれ自身が既に結果となっているオブジェクト。

次に、(+ 2 3)を実行してみよう。

CL-USER> (+ 2 3)
5

括弧に囲まれたものは全部、リストと呼ばれる。

この場合は、+、2、3の3つがリストの内容。

リスト先頭は、特別扱いされて関数の名前という扱いになる。そしてそれ以降は、その関数の引数。

つまり引数2と3が、+という関数に渡されることになる。

しかし、こんなことより最初にやっておかないちゃいけないことがある、

"Hello, World," Lisp Style

プログラム解説書の掟"hello, world"

CL-USER> "hello, world"
"hello, world"

とするだけで、目的は達成できる。

"hello, world"は、ストリングでself-evaluatingオブジェクトなので、評価して"hello, world"が表示される。

もうちょっと普通のHello,Worldの例

format関数を使う。

CL-USER> (format t "hello, world")
hello, world
NIL

hello, worldと表示して、format関数の返り値である、NILが返っている。

NILは、Lispにおける真偽値の偽という意味。

もうちょっと進んで関数を作って表示させてみる。

関数の作成には、defunを使う。

CL-USER> (defun hello-world () (format t "hello, world"))
HELLO-WORLD

関数の名前で使える文字のうちでLisp的なところは、"-"が使えるところ。

hello_worldや、HelloWorldという名前より、hello-worldという名前が好まれる。

実行してみる。

CL-USER> (hello-world)
hello, world
NIL

hello, worldと表示してNILが返っている。

これは、関数の中で最後に評価された値が関数の返り値となるため。この場合はformat関数の返り値。

Saving Your Work

作った関数を保存するには->Emacsでファイルを.lispや、.clというファイルを作成してそれに書く。

SLIME: C-c C-q (slime-close-parens-at-point)

<<最新のCVS版SLIME(Lisp in a Boxではなく単体のもの)では代わりにslime-close-all-parens-in-sexpを使えとのこと。(slime-editing-commands.elの説明より)

SLIME: C-c C-] (slime-close-all-parens-in-sexp)

となっています。>>

SLIME:C-c C-c (slime-compile-defun)

エディタバッファのコンパイルしたい式の上で、C-c C-cを押すと式がコンパイルされる。

とりあえず、hello-world.lispという名前で関数の定義を保存。

一旦slimeを終了させて、再度、M-x slimeで起動。

(hello-world)を評価。

→エラー

ややこしい画面がREPLに表示される。これから抜けるには、"q"を押す。

hello-worldは定義されていないのでエラーになるのです。という訳で、保存したファイルを再度読み込ませる必要あり。

(load "hello-world.lisp")

で読み込ませる。

他の方法として、ファイルをコンパイルしてから、それを読み込ませるという方法もあり。

(load (compile-file "hello-world).lisp")

という感じになる。

上記2つの手順は、SLIMEでショートカットも定義されている

SLIME:C-c C-l (slime-load-file)
 指定したファイルをロード
SLIME:C-c C-k (slime-compile-and-load-file)
 指定したファイルをコンパイルしてからロード

まだまだ色々あるけど、この流れがLisp開発の基本。

REPLを使って新しいコードをロード、テスト、デバッグする。

本格的になると、終日同じ環境でプログラムを開発する人も結構いる。一旦立ち上げられた環境(イメージと呼ばれる)でプログラムは徐々に成長して行く。

REPL武勇伝:160万km彼方のNASAの宇宙探索機 Deep Space 1のバグを遠隔操作のREPLで対話的かつ動的に修正。

2007-12-03

Practical Common Lisp (1)

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

Common Lispの優れた入門書として、評判のPractical Common Lisp

以前、著者のPeter Seibel氏が、Googleの検索キーワード「lisp tutrial」で首位を奪取したいキャンペーンを実施していた時には、このブログでもリンクを貼ってみましたが、めでたく、首位を奪取されたようです

お蔭様で何の関係もないこのブログが「lisp tutorial」で検索するとウェブ全体検索で4位、日本語のページ検索で1位になりました。(引っ越す前のブログ)

どういう仕組みで浮上してしまうのかは謎ですが、何でアクセスも非常に少ないこのサイトが浮上してしまうんでしょうか。

それはさておき日本語訳はまだ出版されておらず、読むとなると英語なので英語が苦手な自分はスルーしてましたが、スルーするのも勿体無いので、自分も読んでみることにしました。

ちなみに、現在、翻訳にTOP PAGEさんが取り組んでいて、10章まで作業は完了しているそうです。

出版されるとすると、本にするというのは何だかんだ大変そうなので、2009年春とかその位でしょうか。

これが出版されればOn Lisp以上にヒットし、Common Lispのユーザが増えて、その影響で日本語環境も整ってさらに快適で身近にCommon Lispでプログラムできるようになるに違いありません!

さて、読み進める方針ですが、英語が苦手ということもあり、コードがあったら、文章よりも先に例示されているコードを暗記して写経してみることにしました。

そのプロセスを通じて何が問題になるかがより明確になるんじゃないかと勝手に考えています。また、英語のハンデも若干補えるんじゃないかと思います。

では早速第一章。

文字だけの章なので、読みながらこのブログにメモとることにしました。以下非常に適当なメモなので、勉強する方は、原書を参照して下さい。

<<>>内は私のツッコミです。

1. Introduction: Why Lisp?

なんでCommon Lispなのかのまえに、著者がLispを使うようになるに至ったお話。

父ちゃんの話。

プログラマだった。

難航していたFortranを機軸にしたプロジェクトに関わった。

時は1980年初頭。AIブームの折、父ちゃんは困難の解決にLispが福音にならないかしらと思った。

Lispの本場の一つであるCMUに相談に行って色々訊いた。

Lispを使ってみることにした。

成功しちゃった。

おれ、息子の話。

そういう父ちゃんの話は知ってはいたが、高校/大学ともに、Lispを学ぶことはなかった。

<<とはいえ、プログラマにはなってるわけで結局似た者親子だな。>>

大学を出てからは、Perl->Java、C、C++という感じで仕事していた。

そんな折、生産性の高い言語について興味が湧いて来た。

というわけで、Lispはどうかと使い始めてみた。

そしたら予想外に生産性が高かった。

例えば…

昔Java初心者の頃に作ったことのある囲碁ゲームをLispで作ってみたが良い感じにすぐできた。

数年の経験を経たJavaで再実装するより生産性は高いと思った。

かつCommon Lispでは、全くの初心者であることを勘案するとグっと来た。

だからみんなもきっとそう感じるに違いない。

Why Lisp?

現場でメジャーに使われている言語との比較が色々。

Lispなら〜が〜よりできるという主張

<<Lisp好きな人が良く言う煽り文句が色々>>

「Lispはそれ自身をプログラム可能なプログラム言語である」ことがミソ。

開発も楽で良いよ。

柔軟性も高くて、拡張も楽で良いよ。

CLOSの例。

もしAOPが今後大流行するとしても何の変更もなしに取り入れることもできるよ。

Where It Began

Lispの歴史の話。

McCarthyの話。

1980年代のAIブームでは、Lispは専ら前人未踏の困難な問題に取り組む研究者の間で重宝された。

自動で問題を解析して問題を解くプログラムなど。

そして冷戦真っ只中であり、国防のためのシミュレーション等需要もあった。

AIバブルだったよ。

しかし、1980年当時にそういう問題は、マシンの能力の制限等でかなり厳しかった。

でも、がんばりました。

そんな努力のお蔭でCommon Lispはパフォーマンスについても良く考えられているよ。

Cと同程度のパフォーマンスもだせるよ。

そうそう、当時、Lispマシンってのもあったよ。

Lisp向きのチップの上にシステム全体もLispで記述されていて、その上は何でもLispで動いてたよ。

それで、1981年頃にLisp方言を纏めようという動きが現われたよ。

そんなこんなで、1986年にCommon Lispの最初の実装が登場したよ。

それで、ANSIでも、1996年にANSI Common Lispとして規格がまとまったよ。

このときには、CLOSとコンディションシステムが規格の範疇に入ったよ。

でもGUIとかマルチスレッド制御とかTCP/IPソケットについては範疇外だよ。

今じゃ、オープンソース関連の人々等が各自必要なものを作って補って使ってるって感じだよね。

Lispは、教科書に載るような非常に古典的な言語といえるかな。一方、実際的な問題を可能な限り効果的に解決できるモダンな言語でもありつづけているよ。

Lispにはインタープリタの実装しかなくて、遅くて、その上、何でも再帰で書かなくちゃいけないっていうのは、70年代始めのことでもいっているのかな。

But I learned Lisp Once, And IT Wasn't Like what you're describing

Schemeとの比較。

Who This Book Is For

Common Lispに興味がある人へ。

学校の勉強で、Lispに触れたけどいまいちだった人へ。

Perl, Python, Java, C, C#を使ってる実際家の人にもCommon Lispは「使える」ことを示したい。

この本は、文法とか言語の機構の説明に終始してないつもり。

最初のパートでは、ちょっとした実践的な例をまじえて言語の説明をして、残りでは、他の本がカバーしてないようなところを実践的な参考にできる例題を載せたよ。

スパムフィルタ、バイナリファイルのパーズ、MP3のカタログ、MP3のストリーミングと、それにウェブのインターフェースをつけたりとか。

この本をマスターしたなら、主要な言語機能には親しんだことになると思うし、Common Lispで些細でないプログラムを作れるようになる筈。

その先は各自探究して下さい。

2007-11-26

CLOSチュートリアル (3)

| 00:57 | CLOSチュートリアル (3) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (3) - わだばLisperになる

Common Lisp クックブックさんのところののCLOSのチュートリアルにて新しい練習問題(CLOSチュートリアル -3.6. サブクラスと継承)が出たので挑戦!。

問題.1

今までの記述で、CLOSの機能のうち defstruct に相当するものすべてに言及してきましたか?

解答

defclass(CLOS)でフォローできるdefstructの機能というところでは、漏れなく言及されたと思うのですが…。見た感じでは、defstructはもう少し多機能なようです。

問題.2

構造体を使っているアプリケーションを取ってきて、defclass を使って書き直しなさい。

解答

ぱっと思い付いたのが、ポール・グレアム氏のANSI Common Lispの二重リンクリスト(日本語版P183辺り)だったので、それをお題に書き直してみることにしました。

(defclass thread ()
  ((prev :initarg :prev :accessor THREAD-uncdr :initform nil)
   (data :initarg :data :accessor THREAD-car :initform nil)
   (next :initarg :next :accessor THREAD-cdr :initarg nil)))

;; なんとなくthreadpを追加
(declaim (inline threadp))
(defun threadp (obj)
  (typep obj 'thread))

(defun thread->list (lst)
  (if (threadp lst)
      (cons (thread-car lst) (thread->list (thread-cdr lst)))
      lst))

(defun thread-insert (x lst)
  (let ((elt (make-instance 'thread :data x :next lst)))
    (when (threadp lst)
      (if (thread-uncdr lst)
          (setf (thread-cdr (thread-uncdr lst)) elt
                (thread-uncdr elt) (thread-uncdr lst)))
      (setf (thread-uncdr lst) elt))
    elt))

(defun thread-cons (&rest args)
  (reduce #'thread-insert args
          :from-end t :initial-value nil))

(defun thread-remove (lst)
  (if (thread-uncdr lst)
      (setf (thread-cdr (thread-uncdr lst)) (thread-cdr lst)))
  (if (thread-cdr lst)
      (setf (thread-uncdr (thread-cdr lst)) (thread-uncdr lst)))
  (thread-cdr lst))

たまたま眺めていたMaclispのソースに二重リンクリストのコードがあって、二重リンクリストのことをスレッドと呼んで、car、cdr、uncdr(その名の通り逆のcdr)としていたので、そういう名前にしてみました。…名前はそのままにしておいた方が良かったような…。結果としては殆ど元のコードそのままで、defstructからの変更は殆どなし。

;; 動作
(setq th (thread-cons 'x 'y 'z))

(thread-car th)
;=> x
(thread->list (thread-cdr th))

;=>(Y Z)

;(thread->list th)

;=> (X Y Z)

(thread->list (thread-remove th))
;=> (Y Z)

defstructと違ってprint-functionの指定ができないようなので、結果を一々thread->listしないと見辛いですが、同じように動作しています。

問題.3

今使っている処理系で、nil のクラス継承階層( class-precedence-list)を調べなさい。

解答

とりあえず、手元の処理系で、

(let ((cpl #+sbcl 'sb-mop:class-precedence-list
	   #+allegro 'mop:class-precedence-list
	   #+lispworks 'class-precedence-list
	   #+clisp 'clos:class-precedence-list))
  (funcall cpl (find-class nil)))

(mapc #'print (sb-mop:class-precedence-list (find-class nil)))

を試してみましたが、全部エラーで、NILってクラスはないよ、ということでした。

;; SBCL
;; => error There is no class named NIL.

;lisp works 
;; => There is no class named NIL.

;; Allegro
;; => Error: No class named: NIL.

;; CLISP
;; => *** - FIND-CLASS: NIL does not name a class

2007-11-23

CLOSチュートリアル (2)

| 20:45 | CLOSチュートリアル (2) - わだばLisperになる を含むブックマーク はてなブックマーク - CLOSチュートリアル (2) - わだばLisperになる

Common Lisp クックブックさんのところのCLOSのチュートリアルで新しい練習問題を見付けたので挑戦!。

問題

CLOSチュートリアル 3.5. スロットより

  • defstruct マクロの実装を探し、CLOSのスロットオプションを一つ以上追加しなさい。

解答

SBCLのマクロを追い掛けてみましたが、どうも既存のマクロに追加するというのはちょっと難しそうなので、defstructをラッピングするdefstruct-plusというマクロを定義してみることにしました。

追加したスロットは、一番簡単そうなところで、:accessorと、:readerの2つです。

(defmacro defstruct-plus (name-and-options &rest slot-descriptions)
  (let ((name (carat name-and-options)))
    `(prog1
	 (defstruct ,name-and-options
	   ,@(mapcar (lambda (x) 
		       (cond ((member :accessor x) (remove-accessor-def x))
			     ((member :reader x) (repl-reader-def x))
			     ('T x)))
		     slot-descriptions))
       ,@(mapcar (lambda (x) 
		   (multiple-value-bind (accessor slot-name reader-p) 
		       (get-accessor-or-reader-name x)
		     (let ((acc-name (symbol-name-conc name "-" slot-name)))
		       (when accessor
			 (if reader-p
			     `(defun ,accessor (obj)
				(,acc-name obj))
			     `(progn
				(defun ,accessor (obj)
				  (,acc-name obj))
				(define-setf-expander ,accessor (obj)
				  (get-setf-expansion 
				 `(,',acc-name ,obj)))))))))
		   slot-descriptions))))

;; defstructには不要な、:accessor引数を除いた引数を返す
(defun remove-accessor-def (args)
  (do ((a args (cdr a)) 
       res)
      ((endp a) (nreverse res))
    (if (eq :accessor (car a))
	(return (nreconc res (cddr a)))
	(push (car a) res))))

;; defstructの形式に合わせて:reader引数を:read-only tに変換する
(defun repl-reader-def (args)
  (do ((a args (cdr a)) 
       res)
      ((endp a) (nreverse res))
    (if (eq :reader (car a))
	(return (nreconc res `(:read-only 'T ,@(cddr a))))
	(push (car a) res))))

;; 関数の名前を付けるための補助関数
(defun symbol-name-conc (&rest names)
  (values
   (intern 
    (string-upcase 
     (apply #'concatenate 'string (mapcar #'string names))))))

(defun carat (obj)
  (if (consp obj) (car obj) obj))

;; :readerか:accessorの場合に与えられた値を返す。
;;  2値目は、スロットの名前
;;  :readerの場合、3値目でtを返す。
(defun get-accessor-or-reader-name (args)
  (let (reader-p)
    (values (or (cadr (member :accessor args))
		(let ((tem (cadr (member :reader args))))
		  (when tem
		    (setq reader-p t)
		    tem)))
	    (car args)
	    reader-p)))

適当に建増しを繰り返していたらどうにも収集がついてない長ったらしいものになってしまいました。

(defstruct-plus foo
  (x 10 :accessor access-foo-x)
  (y 20 :reader reader-foo-y-ro))

(PROG1 (DEFSTRUCT FOO (X 10) (Y 20 :READ-ONLY 'T))
  (PROGN
   (DEFUN ACCESS-FOO-X (OBJ) (FOO-X OBJ))
   (DEFINE-SETF-EXPANDER ACCESS-FOO-X (OBJ)
     (GET-SETF-EXPANSION `(FOO-X ,OBJ))))
  (DEFUN READER-FOO-Y-RO (OBJ) (FOO-Y OBJ)))

のように展開され、普通のdefstructの定義に加えてaccessorかreaderで指定した名前の関数をエイリアスとして作っているような感じです。