Lisp Users and Vendors Conference August 10, 1993 Tutorial on Good Lisp Programming Style Peter Norvig Sun Microsystems Labs Inc. Kent Pitman Harlequin, Inc. Portions copyright (c) 1992, 1993 Peter Norvig. Portions copyright (c) 1992, 1993 Kent M. Pitman. All Rights Reserved. アウトライン 1. 良いスタイルとは何か? 2. 組み込みの機能に関するヒント 3. ほぼ標準のツールに関するヒント 4. 抽象化の種類 5. 大規模なプログラミング 6. その他 1. 良いスタイルとは何か? 良いLispプログラミングスタイル "エレガンスはオプションではない。" - Richard A. O'Keefe (あらゆる言語において)良いスタイルは以下のようなプログラムへ導く: - 理解しやすい - 再利用しやすい - 拡張性がある - 効率的である - 開発/デバッグが容易である それは正確さ、堅牢さ、互換性も助ける 良いスタイルについての我々の金言は: - 明示的に - 具体的に - 簡潔に - 一貫して - 役にたち(読む人のニーズを予期する) - 慣習的に(不明瞭であってはならない) - 利用可能なレベルで抽象を作り - ツールが相互作用することを許す(参照透明性) 良いスタイルはプログラムをサポートする"下着"である 1.1. 良いスタイルはどこから来るのか? 何を信じるのか 我々が話すことの全てを信じてはいけない(まあ多くは信じてほしい)。 何を信じるのかはより少なく、なぜ信じるのかはより多く気にすること。 どこからあなたの"スタイルの規則"が来るのかを知ること: - 宗教、善 vs. 悪 "この方法の方がいい" - 哲学 "これはその他の物事と一貫性がある" - 堅牢性、信頼性、安全性、倫理性 "何か恐ろしいことを避けるために余分な チェックを置こう" - 合法性 "我々の弁護士はこの方法でやれと言う" - 個性、意見 "私はこの方法が好きだ" - 互換性 "別のツールがこの方法を期待する" - 移植性 "他のコンパイラはこの方法を好む" - 協調性、規約 "何らかの統一した方法でなされなければならないので、我々 はこの方法に同意した" - 習慣、伝統 "我々はいつもこの方法でやってきた" - 能力 "私のプログラマたちは十分には洗練されていない" - 記憶 "それをどうやっていたかをわかっているということは、それをどうやっ たかを思い出す必要はないことを意味する" - 迷信 "それを別の方法で行なうのは恐ろしい" - 実用性 "これは他の物事をより容易にする" 全てはコミュニケーションにかかわることである 表現 + 理解 = コミュニケーション プログラムは以下のものとコミュニケーションを行なう: - 人間の読み手 - コンパイラ - テキストエディタ(引数リスト、文書文字列、インデント) - ツール(trace, step, apropos, クロスリファレンス, マニュアル) - プログラムのユーザ(間接的なコミュニケーション) コンテキストを理解する コードを読むときには: - 誰がいつそれを書いたかを理解する。 コードを書くときには: - コメントを用いて注釈を付ける。 - コメントに署名し日付を書き込むこと!(これを行なうのはエディタコマンド であるべき) 注意すべきいくつかのこと: - 人々のスタイルは時間とともに変化する。 - 別の時間での同じ人物は別の人間のように見え得る。 - ときにはその人物はあなたである。 1.2. それが良いかどうかをどうやって知るか? 価値のシステムは絶対ではない スタイルの規則は分離して見ることはできない。 それらはしばしば矛盾する方法で重なっている。 スタイルの規則が相互に矛盾するという事実は、実社会のゴールが矛盾してい るという自然な事実を反映している。良いプログラマは、プログラミングスタ イルにおいて、基となる種々の主要なゴール間の優先度の選択を反映するトレ ードオフを行なう: - 理解可能 - 再利用可能 - 拡張性がある - 効率的(コーディング、空間、速度、...) - 開発/デバッグが容易 なぜ良いスタイルは良いのか 良いスタイルは現在のプログラムを、そして次のプログラムを作ることを助け る: - プログラムを組織化し、人間の記憶の必要を減らす - モジュール化された、再利用可能な部分を促進する スタイルは単に最後に加えられるのではない。それは以下の役割を果たす: - ファイルへのプログラムの組織化 - トップレベル設計、各ファイルの構造とレイアウト - モジュールやコンポーネントへの分解 - データ構造の選択 - 個々の関数の設計/実装 - 命名、整形、そして文書化の標準 なぜスタイルは実用的なのか: 記憶 "若かったころ、それぞれの部屋に10の異なるものがある20の部屋をもつ城を 想像できた。何も問題はなかった。もはやそれはできない。現在では、以前の 経験に照らして考えるようになっている。私は不完全な雲を見るのであり、写 真の絵はがきのようなはっきりしたものではない。しかし、私は確かによりよ いプログラムを書くのである。" - Charles Simonyi "普通の人々よりも多くの詳細を扱えるために良いプログラマである人々がい る。しかし、その理由のためにプログラマを選ぶことは多くの短所があるので ある - 結果として他の誰も保守できないプログラムになり得るからだ。" - Butler Lampson "私のプログラムの中から任意の3行を選んでください。そして、私はそれらが どこにあって何をするものかを言うことができます。" - David McDonald 良いスタイルは、大きな記憶力の必要性にとって代わる: - 任意の3(5? 10?)行が自己説明的であることを確実にする "参照透明性"とも呼ばれる オブジェクトと抽象概念に複雑さをパッケージ化する; グローバルな変数/ 依存性にではない - 上位から下位まで全てにおいて"フラクタルに"自己組織化する - 意図していることを言う - 言っていることを意図する なぜスタイルは実用的なのか: 再利用 構造化プログラミングは、仕様を満たし、その仕様の境界内部で再利用され得 るモジュール化を促進する。 階層化設計は、普通に必要とされる機能をもつモジュール化を促進し、そのモ ジュールは、仕様が変化するときや、別のプログラムの中でさえ再利用され得 る。 オブジェクト指向設計は、オブジェクトのクラスと情報隠蔽に集中する階層化 設計である。 以下の再利用を目指すべきである: - データ型(クラス) - 関数(メソッド) - 制御抽象 - インターフェイス抽象(パッケージ、モジュール) - シンタックス抽象(マクロや言語全体) 意図することを言う "意図することを言うこと。単純に、そして直接的に。" - Kernighan & Plauger データの中で意図することを言う(具体的に、簡潔に): - データ抽象を用いる - 必要なら、データのための言語を定義する - 名前を賢明に選ぶ コードの中で意図することを言う(簡潔に、慣習的に): - 明確にインターフェイスを定義する - マクロや言語を適切に用いる - 組み込みの関数を用いる - 自分の抽象概念を作る - 1度でできるなら2度は行なわない 注釈で(明示的に、有益に) - コメントに対して適切な詳細さを用いる - 文書文字列はコメントよりも良い - 単にそれが何をするかではなく、何のためのものかを言う - 宣言とアサーション - システム(とテストファイル、その他) 明示的に 付加引数とキーワード引数 デフォルト値を調べなければならない場合、それを供給する必要がある。気に しないと本当に信じている、あるいはデフォルトは皆に十分に理解され受け入 れられていると確信している場合でのみデフォルト値をとるべきである。 たとえばファイルをopenするとき、ほとんどけっして:directionキーワード引 数を省略することを考えるべきではない。たとえデフォルトで:inputだとわかっ ていても。 宣言 型情報がわかっている場合、それを宣言すること。他の人々が行なうようなこ とや、コンパイラが使うだろうとわかっているものだけを宣言するようなこと を行なわないこと。コンパイラは変化し、プログラムが進行中の介在を必要と せずにそれらの変更を自然に利用させたい。 また、宣言は人間の読み手とのコミュニケーションのためのものでもある - 単にコンパイラのためではない。 コメント コードを読むときに他の人が知りたいかもしれないもの、かつ、すぐには明ら かでないかもしれない何か有益なものを考えている場合、それをコメントにす ること。 具体的に データ抽象が保証するのと同様に具体的に。それ以上具体的ではいけない。 選択する: ;; より具体的 ;; より抽象的 (mapc #'process-word (map nil #'process-word (first sentences)) (elt sentences 0)) 最も具体的な条件式: - ifは2方向に分岐する式に対して - when, unlessは1方向の文に対して - and, orは論理値に対してのみ - condは複数の方向に分岐する文または式に対して ;; 予想と異なる: ;; 予想と同じ (and (numberp x) (cons x)) (and (numberp x) (> x 3)) (if (numberp x) (cos x)) (if (numberp x) (cos x) nil) (if (numberp x) (print x)) (when (numberp x) (print x)) 簡潔に 最も単純な場合に対してテストすること。2つの場所で同じテストを行なう(ま たは同じ結果を戻す)場合、より容易な方法があるに違いない。 悪い: 冗長、複雑 (defun count-all-numbers (alist) (cond ((null alist) 0) (t (+ (if (listp (first alist)) (count-all-numbers (first alist)) (if (numberp (first alist)) 1 0)) (count-all-numbers (rest alist)))))) - 2度0を戻している - 標準的ではないインデント - alistは連想リストを示唆する 良い: (defun count-all-numbers (exp) (typecase exp (cons (+ (count-all-numbers (first exp)) (count-all-numbers (rest exp)))) (number 1) (t 0))) typecaseの代わりにcondも同じく良い(より具体的でなく、より慣習的であり、 一貫している)。 簡潔に LOCNW(書かれないコードの行, lines of code not written)を最大にする "より短いことはより良く、最も短いことは最も良い。" - Jim Meehan 悪い: 冗長過ぎる、非効率的 (defun vector-add (x y) (let ((z nil) n) (setq n (min (list-length x) (list-length y))) (dotimes (j n (reverse z)) (setq z (cons (+ (nth j x) (nth j y)) z))))) (defun matrix-add (A B) (let ((C nil) m) (setq m (min (list-length A) (list-length B))) (dotimes (i m (reverse C)) (setq C (cons (vector-add (nth i A) (nth i B)) C))))) - nthを使うとO(n^2)になる - なぜlist-lengthなのか? なぜlengthまたはmapcarではないのか? - なぜnreverseではないのか? - なぜ配列を実装するために配列を使わないのか? - 戻り値が隠されている より良い: より簡潔 (defun vector-add (x y) "2つのベクタを要素ごとに加算" (mapcar #'+ x y)) (defun matrix-add (A B) "2つの行列(リストのリスト)を要素ごとに加算" (mapcar #'vector-add A B)) あるいは総称関数を用いる: (defun add (&rest args) "総称関数" (if (null args) 0 (reduce #'binary-add args))) (defmethod binary-add ((x number) (y number)) (+ x y)) (defmethod binary-add ((x sequence) (y sequence)) (map (type-of x) #'binary-add x y)) 有益に 文書はユーザが行なう必要がある仕事の周りに組織化されなければならず、あ なたのプログラムがたまたま提供するものの周りにではない。それぞれの関数 に文書文字列を加えることは、通常はどのようにあなたのプログラムを用いる かをユーザに語らないが、正しい場所でのヒントは非常に効果的であり得る。 良い(GNU Emacsのオンラインヘルプから) next-line: ARG行だけ垂直方向下向きにカーソルを移動する。... Lispプログ ラムの中でこれを用いることを考えている場合、代わりに`forward-line'の使 用を考慮すること。それは通常はより使いやすく、より信頼できる(ゴールの カラムに依存しない等)。 defun: 関数としてNAMEを定義する。その定義は(lambda ARGLIST [DOCSTRING] BODY...)である。関数interactiveも参照。 これらはユーザの使い方と問題を予期している。 慣習的に 既存の機能に平行になるようにあなた自身の機能を作る 命名規約に従う: with-something, dosomethingマクロ 可能なときは組み込みの機能を用いる - 慣習: 読み手はあなたが意図することがわかるだろう - 簡潔: 読み手はコードをパースする必要がない - 効率: 厳しい条件で動作してきた 悪い: 非慣習的 (defun add-to-list (elt lst) (cond ((member elt lst) lst) (t (cons elt lst)))) 良い: 組み込み関数を用いる (練習問題として残してある) "ライブラリ関数を用いよ" - Kernighan & Plauger 一貫して いくつかの操作のペアはオーバーラップする能力をもつ。(どちらも使用され 得るような)中立的な場合に用いるものに関して一貫するようにすること。そ うすれば、何か普通ではないことを行なっているときが明らかである。 ここにletとlet*を伴う例がある。最初のものは並列的な束縛を、2番目のもの は逐次的な束縛を利用する。3番目のものは中立的である。 (let ((a b) (b a)) ...) (let* ((a b) (b (* 2 a)) (c (+ b 1))) ...) (let ((a (* x (+ 2 y))) (b (* y (+ 2 x)))) ...) ここにfletとlabelsを用いる類似の例がある。最初のものは局所関数上のクロ ージャを、2番目のものは非クロージャを利用する。3番目のものは中立的であ る。 (labels ((process (x) ... (process (cdr x)) ...)) ...) (flet ((foo (x) (+ (foo x) 1))) ...) (flet ((add3 (x) (+ x 3))) ...) どちらの場合でも、物事を逆に選択できるだろう。つねにlet*あるいはlabels を中立の場合に、letあるいはfletを普通ではない場合に用いるのである。一 貫性が実際の選択よりも重要である。しかし、多くの人々はletとfletを普通 の選択として考えている。 正しい言語を選ぶ 適切な言語を選び、選んだ言語の中の適切な機能を用いること。Lispは全ての 問題に対して正しい言語ではない。 "あなたは、あなたをつれてきた人と踊ることになる。" - Bear Bryant Lispは以下に対して良い: - 探検的プログラミング - すばやいプロトタイピング - 市場導入までの時間を最少にする - 単一のプログラマ(または10人未満のチーム)のプロジェクト - ソースからソースへ、またはデータからデータへの変形 コンパイラやその他のトランスレータ 問題特有の言語 - 動的なディスパッチや作成(実行時にコンパイラが利用可能) - (Unixの文字パイプモデルとは対照的に)1つのイメージの中へのモジュール の緊密な統合 - 高度の対話性(read-eval-print, CLIM) - ユーザが拡張できるアプリケーション(GNU Emacs) "私は、とても高く密なレベルで互いに影響し合う2人から4人くらいの小さな チームによって良いソフトウェアは書かれると信じる。" - John Warnock "いったん経験豊かなLispプログラマになると、他のどのような言語にも戻る ことは難しい。" - Robert R. Kessler 現在のLispの実装は、以下に対してはそれほど良くない: - 持続性の記憶領域(データベース) - 小さなマシン上での資源の使用を最大にする - 数百名のプログラマを用いるプロジェクト - 別言語のコードとの緊密な通信 - 小さなイメージのアプリケーションを配達する - 実行時の制御(しかしGensymはそれを行なった) - 経験不足のLispプログラマを用いるプロジェクト - ある種の数値または文字計算(注意深く宣言を行なうことでうまく動作する が、Lispの効率のモデルは学習しづらい) 2. 組み込みの機能に関するヒント 組み込みの機能 "疑う余地のないことだが、Common Lispは大きな言語である" - Guy Steele - 622の組み込み関数(あるANSI以前のCLで) - 86のマクロ - 27の特殊形式 - 54の変数 - 62の定数 しかし、言語自身として何を数えるのか? - C++は48の予約語をもつ - ANSI CLは25の特殊形式に減っている - 残りは必須のライブラリとして考えられる どちらにしても、Lispプログラマは何らかの助けを必要とする: どの組み込みの機能を利用するか それをどのように使うか DEFVARとDEFPARAMETER 再ロード時に再初期化したくない物事に対してdefvarを使うこと。 (defvar *options* '()) (defun add-option (x) (pushnew x *options*)) ここではファイルを再ロードする前に何回も(add-option ...)を行なったかも しれない - おそらく何回かは別のファイルからかもしれない。通常は、この 定義を再ロードするためだけにそのデータの全てを捨て去りたくはないだろう。 他方で、ある種のオプションは再ロードで再初期化されてほしい... (defparameter *use-experimental-mode* nil "実験的なコードを動かすときはこれをTに設定すること。") 後で、このファイルを編集して、その変数をTに設定し、それから編集の効果 を見ることを望んで再ロードするかもしれない。 推薦: defvarは変数のためのものでdefparameterはパラメータのためのものだ と述べているCLtLの部分は無視すること。これらの間の唯一の有益な違いは、 defvarは変数が束縛されていない場合にのみ代入を行なうが、defparameterは 無条件に代入を行なうことである。 EVAL-WHEN (eval-when (:execute) ...) = (eval-when (:compile-toplevel) ...) + (eval-when (:load-toplevel) ...) また、eval-whenフォームを明示的に入れ子にすることに気をつけること。そ の効果は一般に多くの人々にとって直観的ではない。 コードの重複を避けるためのFLET 以下の例の(f (g (h)))の重複した使用を考えよう。 (do ((x (f (g (h))) (f (g (h))))) (nil) ...) (f (g (h)))の一方を編集するたびに、おそらく他方も編集したいだろう。よ り良いモジュール性は以下である: (flet ((fgh () (f (g (h))))) (do ((x (fgh) (fgh))) (nil) ...)) (これはdoに対する議論として用いられるかもしれない。) 同様に、動的な状態でのみ異なるコードブランチでの重複を避けるために局所 関数を使えるかもしれない。たとえば、 (defmacro handler-case-if (test form &rest cases) (let ((do-it (gensym "DO-IT"))) `(flet ((,do-it () ,form)) (if test (handler-case (,do-it) ,@cases) (,do-it))))) DEFPACKAGE 大規模なプログラミングは、はっきりと定義されたインターフェイスを用いて コードをモジュールに分離するデザインスタイルによってサポートされる。 Common Lispのパッケージシステムは、モジュール間の名前の衝突を避けるた めに、そして各モジュール間のインターフェイスを定義するために役立つ。 - トップレベルはない(スレッドセーフにする) - 他のプログラムがある(パッケージを用いる) - 消費者にとって使いやすいものにする 消費者が必要とするものだけをエキスポートする - 保守担当者にとって使いやすいものにする エキスポートしていない部分の変更を許可する (defpackage "PARSER" (:use "LISP" #+Lucid "LUL" #+Allegro "EXCL") (:export "PARSE" "PARSE-FILE" "START-PARSER-WINDOW" "DEFINE-GRAMMAR" "DEFINE-TOKENIZER")) 定義されたファイルの先頭にエキスポートされたシンボルを置く人もいる。 我々はdefpackageの中にそれらを置くこと方が良いと感じる。そして、対応す る定義を見つけるためにはエディタを用いる。 コンディションとエラーの違いを理解する Lispは、能動的なコンディションシステムを提供することで、コードの中のほ とんどのエラーがデータを破壊しないことを保証する。 エラーとコンディションの違いを学ぶこと。 全てのエラーはコンディションである; 全てのコンディションがエラーである とはかぎらない。 3つの概念を区別する: - コンディションの通知 - 何か普通でないことの発生を検出する。 - 再起動の提供 - 継続するためのおそらくいくつかの選択肢の一つを確立する。 - コンディションのハンドリング - 利用可能な選択肢からどのように進むかを選択する。 エラーの検出 あなたの意図に合うエラー検出やハンドリングのレベルを選ぶこと。 通常、あなたは悪いデータが見過ごされるのを許したくはないだろうが、多く の場合、取るに足らない理由のためにデバッガの中に入っていたくもないだろ う。 あなたのアプリケーションに適切な我慢と選択の間のバランスをとること。 悪い: 整数でなかったらどうなるか? (defun parse-date (string) "文字列から日付を読む。 ..." (multiple-value-bind (day-of-month string-position) (parse-integer string :junk-allowed t) ...)) 疑わしい: メモリを使い果たしたらどうなるか? (ignore-errors (parse-date string)) より良い: 予期されるエラーだけを捕らえる (handler-case (parse-date string) (parse-error nil)) 良いエラーメッセージを書く - エラーメッセージの中では完全な文を用いる(大文字で始め、ピリオドで終 わる)。 - "Error: "あるいは";;"のような接頭辞は不要である。システムが必要なら そのような接頭辞を供給するだろう。 - 新たな行に対する要求でエラーメッセージを始めないこと。システムは必要 ならば自動的にこれを行なう。 - 他のformat文字列と同様に、埋め込みのタブ文字を使わないこと。 - エラーメッセージの中で結果について言及しないこと。単に状況自身を記述 すること。 - どのように継続するかの記述において、デバッガのユーザインターフェイス を事前に仮定しないこと。これは、異なる実装は異なるインターフェイスを 用いるので移植の問題を起こし得る。単に与えられるアクションの抽象的な 効果を記述すること。 - メッセージの中に、他のエラーから区別するのに十分な、可能ならば後に起 きる場合にその問題のデバッグを助けるのに十分な詳細を明記する。 悪い: (error "~%>> エラー: あーあ。継続するには:Cをタイプしてください。") より良い: (cerror "代わりの文を対話的に指定してください。" "不正な形式の文がありました:~% ~A" sentence) コンディションシステムを用いる これらから始める: - error, cerror - warn - handler-case - with-simple-restart - unwind-protect 良い: warnの標準的な使い方 (defvar *word* '?? "現在作業している語。") (defun lex-warn (format-str &rest args) "レキシカルな警告; warnと同様だが、どの語が警告を引き起こしたかをまず言う。" (warn "For word ~a: ~?" *word* format-str args)) HANDLER-CASE, WITH-SIMPLE-RESTART 良い: 特定のエラーをハンドルする (defun eval-exp (exp) "可能ならこの式を評価する; そうでなければそれを戻す。" ;; 式の評価中のエラーからガードする (handler-case (if (and (fboundp (op exp)) (every #'is-constant (args exp))) (eval exp) exp) (arithmetic-error () exp))) 良い: 再起動を提供する (defun top-level (&key (prompt "=> ") (read #'read) (eval #'eval) (print #'print)) "read-eval-printループ。" (with-simple-restart (abort "トップレベルから脱出する。") (loop (with-simple-restart (abort "トップレベルのループに戻る。") (format t "~&~a" prompt) (funcall print (funcall eval (funcall read))))))) UNWIND-PROTECT unwind-protectは、誰もが使い方を知るべき重要な機能を実装する。それはシ ステムプログラマのためだけのものではない。 しかしマルチタスクに注意すること。たとえば、unwind-protectを用いてある 種の状態束縛を実装することはシングルスレッドの環境ではうまく動作するか もしれないが、マルチタスクの環境ではしばしばもう少し注意深くなければな らない。 (unwind-protect (progn form1 form2 ... formn) (cleanup1 cleanup2 ... cleanupn)) - form1が実行されると仮定しないこと。 - formnが実行して完了することはないと仮定しないこと。 しばしば、unwind-protectに入る前に状態をセーブし、状態をリストアする前 にテストする必要がある: おそらく悪い: (マルチタスクで) (catch 'robot-op (unwind-protect (progn (turn-on-motor) (manipulate)) (turn-off-motor))) 良い: (より安全) (catch 'robot-op (let ((status (motor-status motor))) (unwind-protect (progn (turn-on-motor motor) (manipulate motor)) (when (motor-on? motor) (turn-off-motor motor)) (setf (motor-status motor) status)))) I/Oの問題: FORMATを用いる - format文字(または出力を意図するあらゆる文字列)の中にタブ文字を使わな いこと。出力がどのカラムから始まるかかに依存して、タブストップはコー ドの中と同じ出力に並ばないかもしれない! - 読み込み不能なオブジェクトを印字するために"#<~S ~A>"を使わないこと。 代わりにprint-unreadable-objectを使うこと。 - 囲まれている小文字のテキストから目立たせるために、大文字でformat指示 子を置くことを考慮すること。 たとえば、"Foo: ~a"ではなく"Foo: ~A"とする。 - 有用なイディオムを学ぶこと。たとえば: ~{~A~^, ~}や~:pである。 - ~&と~%をいつ使うかを意識すること。 また、"~2%"や"~2&"も便利である。 単一の行を出力する多くのコードは~&で始まり、~%で終わるべきである。 (format t "~&これはテストです。~%") これはテストです。 - 実装の拡張に気をつけること。それらは移植可能ではないかもしれないが、 移植しないコードに対しては非常に有用かもしれない。たとえば、Generaの インデントを扱うための→と←である。 正しくストリームを用いる - *standard-output*および*standard-input* vs *terminal-io* *standard-input*や*standard-output*が*terminal-io*(または、実際には、 あらゆる対話的なストリーム)に束縛されているだろうと仮定しないこと。 しかし、そのようなストリームに束縛することは可能である。入出力のため に直接*terminal-io*を使わないようにすること。それは、主として、それ に対して他のストリームが束縛されているかもしれない、あるいは(たとえ ばシノニムストリームによって)間接的であるかもしれないストリームとし て利用可能である。 - *error-output* vs *debug-io* ユーザとの対話を全く伴わない警告やエラーメッセージに対して *error-output*を用いること。 対話的な警告やエラーメッセージ、そしてプログラムの通常の機能に関係し ないその他の対話に対して*debug-io*を用いること。 特に、*error-output*と*debug-io*が同じストリームであると予期して、最 初に*error-output*にメッセージを印字し、それから*debug-io*上でデバッ ギングセッションを行なわないこと。 その代わりに、一つのストリーム上で一貫してそれぞれの対話を行なうこと。 - *trace-output* これは、単にトレースの出力を受け取る以上のものに対して使うことができ る。条件によっては実行中のプログラムを止めずに有用な情報を印字するデ バッギングルーチンを書く場合、*trace-output*がリダイレクトされる場合 にデバッグ出力もリダイレクトされるように、このストリームへの出力を考 慮すること。 有用なテスト: 誰かがあなたが使用中のいくつかのI/Oストリームの一つだけ を再束縛する場合、あなたの出力が間抜けに見えるようになるだろうか? 3. ほぼ標準のツールに関するヒント ほぼ標準のツールを用いる 言語に組み込まれてはいないが、多くのプログラマに使われている機能がある。 これは言語への拡張と、プログラム開発を補助するツールに分かれる。 拡張 - プログラムを定義するためのdefsystem - CLIM, CLXなどのグラフィックスライブラリ ツール - FSF, Lucidからのemacs インデント、フォント/カラーのサポート 定義/引数リスト/文書/正規表現の発見 lispとの通信 - CMUからのxref, manual等 - ベンダからのブラウザ、デバッガ、プロファイラ DEFSYSTEM defsystemのパブリックドメイン版を選ぶ(不幸にも、dpANS CLは標準をもたな い)。 - 一つの場所にだけ絶対パス名を置く - defsystemを通じてあらゆるものをロードする - ロードすることからコンパイルすることを区別する - オプションでバージョン制御を行なう (defpackage "PARSER" ...) (defsystem parser (:source "/lab/indexing/parser/*") (:parts utilities "macros" "grammar" "tokenizer" "optimizer" "debugger" "toplevel" #+CLIM "clim-graphics" #+CLX "clx-graphics")) - あなたのシステムがコンパイラの警告なしでロードされることを確実にする (最初のとき、そしてそれ以降のとき) ((declare (ignore ...))の使用を学ぶこと) - システムが何もないところからコンパイルされ得ることを確実にする (なかなか消えないブートストラップの問題を取り除く) エディタコマンド あなたのエディタは以下のことができなければならない: - S式に関して移動し、マッチする括弧を示す - 正しくコードをインデントする - バランスがとれていない括弧を見つける - フォントや色を用いてコードを装飾する - あらゆるシンボルの定義を見つける - あらゆるシンボルに対する引数あるいは文書を見つける - あらゆる式をmacroexpandする - 現在の式、リージョンあるいはファイルを、評価またはコンパイルするため にLispへ送る - Lispへ送ったコマンドのヒストリを保存し、編集して再び送ることを許す - キーボード、マウス、そしてメニューを用いて動作する Emacsはこれらの全てことを行なえる。あなたのエディタができない場合、修 正されるまで不平を言うか、新たなものを入手しよう。 Emacs: インデントとコメント 自分でインデントしようとしないこと。 その代わりに、エディタにインデントさせること。 ほぼ標準の形式が進化してきた。 - 80カラムが最大幅 - コメントの慣習に従う ; インラインコメントに対して ;; 関数内のコメントに対して ;;; 関数間のコメントに対して ;;;; (アウトラインモードのための)セクションヘッダに対して - どのようにインデントするかをcl-indentライブラリに言うことができる (put 'defvar 'common-lisp-indent-function '(4 2 2)) - lemacsはフォント、色を提供できる (hilit::modes-list-update "Lisp" '((";;.*" nil hilit2) ...)) 4. 抽象化 抽象 全てのプログラミング言語は、プログラマが抽象を定義することを許す。全て の現代的な言語は以下に対するサポートを提供する: - データ抽象(抽象データ型) - 関数抽象(関数、手続き) クロージャをもつLispやその他の言語(たとえば、ML, Sather)は以下をサポー トする: - 制御抽象(イテレータやその他の新たな制御フロー構造を定義する) Lispはそれがサポートする程度において他に類を見ない: - シンタックス抽象(マクロ、完全に新たな言語) 設計: どこからスタイルは始まるか "プログラムを書くことの中でもっとも重要な部分は、データ構造を設計する ことである。その次に重要な部分は、様々なコード片に分解していくことであ る。" - Bill Gates "熟練したエンジニアは複雑な設計を階層化する。 … それぞれのレベルで構 成される部分は、次のレベルでプリミティブとして用いられる。階層化設計の それぞれのレベルは、その詳細のレベルに適切な種々のプリミティブと結合の 手段をもつ特殊化された言語として考えられる。" - Harold AbelsonとGerald Sussman "可能なかぎり決定を分解すること。表面的に無関係な面だけをほどくこと。 可能なかぎり表現の詳細に関係するそれらの決定を遅らせること。" - Niklaus Wirth Lispはこれらのアプローチの全てをサポートする: - データ抽象: クラス、構造体、deftype - 関数抽象: 関数、メソッド - インターフェイス抽象: パッケージ、クロージャ - オブジェクト指向: CLOS、クロージャ - 階層化設計: クロージャ、上記の全て - 遅延された決定: 実行時のディスパッチ 設計: 分解 "Lispの手続きはパラグラフのようなものである。" - Deborah Tatar "一つの文であらゆるモジュールを説明できなければならない。" - Wayne Ratliff - 単純な設計のために努力する - 問題を部分に分解する 有益な副部分を設計する(階層化) 便宜主義的になること; 既存のツールを用いること - 依存性を決定する 依存性を減らすために再モジュール化する もっとも依存性が高い部分を最初に設計する 我々は以下の抽象の種類を扱う: - データ抽象 - 関数抽象 - 制御抽象 - シンタックス抽象 データ抽象 たまたま実装の中に存在する型ではなく、問題のデータ型の言葉でコードを書 くこと。 - レコード型に対してはdefstructまたはdefclassを用いる - 別名としてインライン関数を用いる(マクロは使わない) - deftypeを用いる - 効率および/または文書化のために宣言や:typeスロットを用いる - 変数名は非公式な型情報を与える かなり良い: いくつかの型情報を指定している (defclass event () ((starting-time :type integer) (location :type location) (duration :type integer :initform 0))) より良い: 問題に特有の型情報 (deftype time () "秒での時刻" 'integer) (defconstant +the-dawn-of-time+ 0 "Midnight, January 1, 1900") (defclass event () ((starting-time :type time :initform +the-dawn-of-time+) (location :type location) (duration :type time :initform 0))) 4.1. データ抽象 抽象データ型を用いる アクセサをもつ抽象データ型を導入する: 悪い: はっきりしないアクセサ、eval (if (eval (cadar rules)) ...) より良い: アクセサのための名前を導入する (declaim (inline rule-antecedent)) (defun rule-antecedent (rule) (second rule)) (if (holds? (rule-antecedent (first rules))) ...) 通常は最良: ファーストクラスのデータ型を導入する (defstruct rule name antecedent consequent) または (defstruct (rule (:type list)) name antecedent consequent) または (defclass rule () (name antecedent consequent)) 抽象データ型を実装する 共通の抽象データ型からLispの実装へどのように対応づけるかを知る。 - 集合: リスト、ビットベクタ、整数、あらゆるテーブル型 - 列: リスト、ベクタ、遅延評価ストリーム - スタック: リスト、(フィルポインタをもつ)ベクタ - キュー: tconc、(フィルポインタをもつ)ベクタ - テーブル: ハッシュテーブル、連想リスト、属性リスト、ベクタ - 木、グラフ: コンス、構造体、ベクタ、隣接行列 すでにサポートされている実装を用いる(たとえば、リストとしての集合に対 してはunion, intersection, length; 整数としての集合に対してはlogior, logand, logcount)。 プロファイリングがボトルネックを明らかにする場合、新たな実装を作ること を恐れないこと(Common Lispのハッシュテーブルがあなたのアプリケーション に対してあまりに効率が悪い場合、Cで特殊化されたハッシュテーブルを作る 前に、Lispでの特殊化されたハッシュテーブルの作成を考慮すること)。 データ型からの継承 直接の利用と同様に継承によって再利用する - 構造体は単一継承をサポートする - クラスは多重継承をサポートする - ともに何らかの優先を許す - クラスはmixinをサポートする クラスまたは構造体を、プログラム全体に対して以下のことを考慮すること - グローバル変数をばらまくことを取り除く - スレッドセーフ - 継承および修正され得る 4.2. 関数抽象 関数抽象 それぞれの関数は以下をもたなければならない: - 単一で特定の目的 - 可能なら、一般的に有用な目的 - 意味のある名前(recurse-auxのような名前は問題の兆候である) - 理解が容易な構造 - 単純だが十分に一般的なインターフェイス - 可能なかぎり少ない依存性 - 文書文字列 分解 アルゴリズムを、単純で意味があり有益な関数に分解すること。 loop vs. mapのcomp.lang.lispでの議論の例: (defun least-common-superclass (instances) (let ((candidates (reduce #'intersection (mapcar #'(lambda (instance) (clos:class-precedence-list (class-of instance))) instances))) (best-candidate (find-class t))) (mapl #'(lambda (candidates) (let ((current-candidate (first candidates)) (remaining-candidates (rest candidates))) (when (and (subtypep current-candidate best-candidate) (every #'(lambda (remaining-candidate) (subtypep current-candidate remaining-candidate)) remaining-candidates)) (setf best-candidates current-candidate)))) candidates) best-candidate)) とても良い: Chris Riesbeck (defun least-common-superclass (instances) (reduce #'more-specific-class (common-superclasses instances) :initial-value (find-class 't))) (defun common-superclasses (instances) (reduce #'intersection (superclass-lists instances))) (defun superclass-lists (instances) (loop for instance in instances collect (clos:class-precedence-list (class-of instance)))) (defun more-specific-class (class1 class2) (if (subtypep class2 class1) class2 class1)) - それぞれの関数はとても理解しやすい - 制御構造が明快である: 二つのreduce、一つのintersectionと一つのloop/collect - しかし、再利用性はかなり低い 同じく良い: そしてより再利用しやすい (defun least-common-superclass (instances) "Find a least class that all instances belong to." (least-upper-bound (mapcar #'class-of instances) #'clos:class-precedence-list #'subtypep)) (defun least-upper-bound (elements supers sub?) "Element of lattice that is a super of all elements." (reduce #'(lambda (x y) (binary-least-upper-bound x y supers sub?)) elements)) (defun binary-least-upper-bound (x y supers sub?) "Least upper bound of two elements." (reduce-if sub? (intersection (funcall supers x) (funcall supers y)))) (defun reduce-if (pred sequence) "E.g. (reduce-if #'> numbers) computes maximum" (reduce #'(lambda (x y) (if (funcall pred x y) x y)) sequence)) - それぞれの関数は理解しやすいままである - なおも2つのreduce、一つのintersectionと一つのmapcarである - 階層化設計がより有益な関数を生み出す 英語翻訳のルール 意図していることを言うことを確実にするために: 1. 英語のアルゴリズムの記述で始める 2. その記述からコードを書く 3. そのコードを英語に翻訳する 4. 3を1と比較する 例: 1. "monsterのリストを与えられ、swarmの数を決定する。" 2. (defun count-swarm (monster-list) (apply '+ (mapcar #'(lambda (monster) (if (equal (object-type (get-object monster)) 'swarm) 1 0)) monster-list))) 3. "monsterのリストをとり、そのタイプがswarmであるmonsterに対して1を、 それ以外には0を生成する。それから数のリストを合計する。" より良い: 1. "monsterのリストを与えられ、swarmの数を決定する。" 2. (defun count-swarms (monster-names) "monster名のリストの中のswarmを数える。" (count-if #'swarm-p monster-names :key #'get-object)) or (count 'swarm monster-names :key #'get-object-type) or (loop for name in monster-names count (swarm-p (get-object monster))) 3. "monster名のリストを与えられ、swarmである数を数える。" ライブラリ関数を使う ライブラリは低いレベルの効率的なハックへのアクセスをもつかもしれず、し ばしば微調整されている。 しかし、それらはあまりにも一般的であるかもしれず、その故に非効率かもし れない。 効率が問題であるときには特定のバージョンを書くこと。 良い: 特定的、簡潔 (defun find-character (char string) "文字が文字列の中に現れるかどうかを調べる。" (find char string)) 良い: 効率的 (defun find-character (char string) "文字が文字列の中に現れるかどうかを調べる。" (declare (character char) (simple-string string)) (loop for ch across string (when (eql ch char) return ch))) nをn個のxのリストに対応づけるbuild1が与えられている: (build1 4) => (x x x x) 任務: 以下になるようにbuild-itを定義せよ: (build-it '(4 0 3)) => ((x x x x) () (x x x)) 信じられないほど悪い: (defun round3 (x) (let ((result '())) (dotimes (n (length x) result) (setq result (cons (car (nthcdr n x)) result))))) (defun build-it (arg-list) (let ((result '())) (dolist (a (round3 arg-list) result) (setq result (cons (build1 a) result))))) 問題: - round3は単にreverseに対する別の名前である - (car (nthcdr n x))は(nth n x)である - dolistの方がここではdotimesより良いだろう - pushがここでは適切だろう - (mapcar #'build1 numbers)がその全てを行なう 4.3. 制御抽象 制御抽象 多くのアルゴリズムは以下のように特徴づけられ得る: - 検索(some find find-if mismatch) - 整列(sort merge remove-duplicates) - フィルタリング(remove remove-if mapcan) - マッピング(map mapcar mapc) - 結合(reduce mapcan) - 数えあげ(count count-if) これらの関数は普通の制御のパターンを抽象化する。 それらを用いるコードは: - 簡潔 - 自己記述的 - 理解しやすい - しばしば再利用可能 - 通常は効率的 (非末尾再帰より良い) あなた自身の制御抽象を導入することは階層化設計の重要な部分である。 再帰 vs. 繰り返し 再帰は再帰的なデータ構造に対して良い。多くの人々はリストを列として見る ことを、そしてそれの上に繰り返しを用いることを好み、したがって、リスト が先頭と残りに分割されるという実装の詳細を強調しないことを好む。 表現力豊かなスタイルとして、末尾再帰がエレガントだとしばしば考えられて いる。しかし、Common Lispは末尾再帰の除去を保証しないので、完全に移植 可能なコードの中では繰り返しの代替として使われるべきではない(Schemeの 中では問題ない)。 Common Lispのdoマクロは末尾再帰に対するシンタックスシュガーとして考え られる。変数に対する初期値は最初の関数呼び出しでの引数の値であり、ステッ プ値はそれ以後の関数呼び出しに対する引数の値である。 doは、抽象化の低いレベルだが多目的に使えるものを提供し、単純で明示的な 実行モデルをもつ。 悪い: (Common Lispで) (defun any (lst) (cond ((null lst) nil) ((car lst) t) (t (any (cdr lst))))) より良い: 慣習的、簡潔 (defun any (list) "リストのいずれかのメンバが真である場合に真を戻す。" (some #'not-null list)) または (find-if-not #'null lst) または (loop for x in list thereis x) または (明示的) (do ((list list (rest list))) ((null list) nil) (when (first list) (return t))) 最良: 効率的、この場合ではもっとも簡潔 anyを全く呼び出さないこと! (any (mapcar p list))の代わりに(some p list)を用いる LOOP "loopを一つのトピックに保つこと - 上院議員への書簡のように" - Judy Anderson Common Lispのloopマクロは、慣用的な使用方法を簡潔に表現する力を与える。 しかし、そのシンタックスとセマンティックスがしばしばおおむねその代替よ りも複雑であるという重荷をもつ。 loopマクロを用いるかどうかは論争に囲まれている問題であり、宗教戦争に近 い。争いの根本には以下のいくぶん逆説的な観察がある: - 英語に似ているように見え、代替よりもあまりプログラミングの知識を必要 としないように思えるので、loopは経験の少ないプログラマにアピールする。 - loopは英語ではない; そのシンタックスやセマンティックスは、多くのプロ グラミングバグの源となってきた微妙な複雑さをもつ。その学習と理解に時 間を費やした人々によって - 通常は経験の少ないプログラマではない - し ばしばもっともうまく使われる。 loopの独自の機能(たとえば、異なる種類の並列の繰り返し)を用いること。 単純な繰り返し 悪い: 冗長、制御構造が不明確 (LOOP (SETQ *WORD* (POP *SENTENCE)) ; 次の単語を得る (COND ;; これ以上の単語がない場合、変数*CONCEPT*に ;; 格納されている実体化されたCDフォームを戻す ((NULL *WORD*) (RETURN (REMOVE-VARIABLES (VAR-VALUE '*CONCEPT*)))) (T (FORMAT T "~%~%Processing ~A" *WORD*) (LOAD-DEF) ; この単語の元で ; 要求を調べる (RUN-STACK)))) ; 要求を行なう - グローバル変数は不要 - 終了テストが誤解を招く - それぞれの単語に何をするのかがすぐには明らかではない 良い: 慣習的、簡潔、明示的 (mapc #'process-word sentence) (remove-variables (var-value '*concept*)) (defun process-word (word) (format t "~2%Processing ~A" word) (load-def word) (run-stack)) マッピング 悪い: 冗長 ; (extract-id-list 'l_user-recs) ------------- [lambda] ; 引数: l_user-recsはユーザレコードのリスト ; 戻り値: l_user-recsの全てのユーザidのリスト ; 使用: extract-id ; 被使用: process-users, sort-users (defun extract-id-list (user-recs) (prog (id-list) loop (cond ((null user-recs) ;; id-listはconsを用いて逆順に構築されたので、 ;; この時点で逆転しなければならない: (return (nreverse id-list)))) (setq id-list (cons (extract-id (car user-recs)) id-list)) (setq user-recs (cdr user-recs)) ; 次のユーザレコード (go loop))) 良い: 慣習的、簡潔 (defun extract-id-list (user-record-list) "ユーザのリストに対してユーザIDを戻す。" (mapcar #'extract-id user-record-list)) 数えあげ 悪い: 冗長 (defun size () (prog (size idx) (setq size 0 idx 0) loop (cond ((< idx table-size) (setq size (+ size (length (aref table idx))) idx (1+ idx)) (go loop))) (return size))) 良い: 慣習的、簡潔 (defun table-count (table) ; 以前はSIZEと呼ばれていた "ハッシュのようなテーブルのキーの数を数える。" (reduce #'+ table :key #'length)) また、以下を加えるのも悪くないかもしれない: (deftype table () "テーブルはバケットのベクタであり、各バケットは (key . values)のペアの連想リストを保持する。" '(vector cons)) フィルタリング 悪い: 冗長 (defun remove-bad-pred-visited (l badpred closed) ;;; Lの中で悪くなくCLOSEDリストにない ;;; ノードのリストを戻す。 (cond ((null l) l) ((or (funcall badpred (car l)) (member (car l) closed)) (remove-bad-pred-visited (cdr l) badpred closed)) (t (cons (car l) (remove-bad-pred-visited (cdr l) badpred closed))))) 良い: 慣習的、簡潔 (defun remove-bad-or-closed-nodes (nodes bad-node? closed) "悪いノード、またはclosedリストにあるノードを取り除く。" (remove-if #'(lambda (node) (or (funcall bad-node? node) (member node closed))) nodes)) 制御フロー: 単純に保つ 非局所的な制御フローは理解が難しい 悪い: 冗長、参照透明性に違反している (defun isa-test (x y n) (catch 'isa (isa-test1 x y n))) (defun isa-test1 (x y n) (cond ((eq x y) t) ((member y (get x 'isa)) (throw 'isa t)) ((zerop n) nil)) (t (any (mapcar #'(lambda (xx) (isa-test xx y (1- n))) (get x 'isa))))) 問題: - catch/throwは根拠がない - memberテストは助けになるかもしれないし、ならないかもしれない - mapcarはゴミを生成する - anyテストは遅過ぎる; throwがこれを修復しようとする その結果、anyはけっして呼び出されない! 単純に保つ catchとthrowを使用に対するいくつかの忠告がある: - マクロとしてより抽象的な制御構造を実装するときに副プリミティブとして catchとthrowを用いること。通常のコードの中では用いないこと。 - ときどき、catchを設定するとき、プログラムはその存在をテストする必要 があるかもしれない。その場合、再起動がより適切かもしれない。 良い: (defun isa-test (sub super max-depth) "SUBがmax-depthより短いISAリンクのチェーンによって SUPERへリンクされているかどうかをテストする" (and (>= max-depth 0) (or (eq sub super) (some #'(lambda (parent) (isa-test parent super (- max-depth 1))) (get sub 'isa))))) これも良い: ツールを用いる (defun isa-test (sub super max-depth) (depth-first-search :start sub :goal (is super) :successors #'get-isa :max-depth max-depth)) "明快に書くこと - 才走り過ぎてはいけない。" - Kernighan & Plauger 意識的に: 何かを"改善すること"はセマンティックスを変更するか? それは問題だろうか? 複雑なラムダ式を避ける 高階関数が複雑なラムダ式を必要とするかもしれないとき、別の方法を考える こと: - dolistまたはloop - 中間の(ゴミの)列を生成する - シリーズ - マクロまたはリードマクロ - 局所関数 - 特定的: どこで関数が使われるかを明快にする - 大域的な名前空間を散らかさない - 局所変数は引数である必要はない - しかし: いくつかのデバッギングツールは動作しない 整数のリストの中の奇数の平方の合計を見つけ出す: 全て良い: (reduce #'+ numbers :key #'(lambda (x) (if (oddp x) (* x x) 0))) (flet ((square-odd (x) (if (oddp x) (* x x) 0))) (reduce #'+ numbers :key #'square-odd)) (loop for x in list when (oddp x) sum (* x x)) (collect-sum (choose-if #'oddp numbers)) 以下も考慮すること: (しばしば適切であるかもしれない) ;; リードマクロを導入する: (reduce #'+ numbers :key #L(if (oddp _) (* _ _) 0)) ;; 中間のゴミを生成する: (reduce #'+ (remove #'evenp (mapcar #'square numbers))) 関数型 vs. 命令型スタイル 命令型スタイルのプログラムは、理解することが難しいと議論されてきた。以 下は命令型のアプローチから生じるバグである: 仕事: 組み込み関数findの別バージョンを書く。 悪い: 正しくない (defun i-find (item seq &key (test #'eql) (test-not nil) (start 0 s-flag) (end nil) (key #'identity) (from-end nil)) (if s-flag (setq seq (subseq seq start))) (if end (setq seq (subseq seq 0 end))) ...) 問題: - とっているサブシーケンスはゴミを生成する - listとvectorの違いを理解していない - startとendの両方が与えられた場合エラーである エラーはseqへの更新から発生する 例: 簡約 仕事: 論理式に対する簡約 (simp '(and (and a b) (and (or c (or d e)) f))) => (AND A B (OR C D E) F) 悪くはないが、完全ではない: (defun simp (pred) (cond ((atom pred) pred) ((eq (car pred) 'and) (cons 'and (simp-aux 'and (cdr pred)))) ((eq (car pred) 'or) (cons 'or (simp-aux 'or (cdr pred)))) (t pred))) (defun simp-aux (op preds) (cond ((null preds) nil) ((and (listp (car preds)) (eq (caar preds) op)) (append (simp-aux op (cdar preds)) (simp-aux op (cdr preds)))) (t (cons (simp (car preds)) (simp-aux op (cdr preds)))))) 式を簡約するプログラム 問題: - simp-auxに対する意味のある名前がない - 再利用可能な部分がない - データアクセサがない - (and)や(and a)が簡約されない より良い: 利用可能なツール (defun simp-bool (exp) "論理(and/or)式を簡約化する。" (cond ((atom exp) exp) ((member (op exp) '(and or)) (maybe-add (op exp) (collect-args (op exp) (mapcar #'simp-bool (args exp))))) (t exp))) (defun collect-args (op args) "与えられたオペレータopをもつ引数を繋げながら、 引数のリストを戻す。結合的オペレータをもつ 式の簡約化のために有用である。" (loop for arg in args when (starts-with arg op) nconc (collect-args op (args arg)) else collect arg)) 再利用可能なツールを作る (defun starts-with (list element) "これは与えられた要素で始まるリストか?" (and (consp list) (eql (first list) element))) (defun maybe-add (op args &optional (default (get-identity op))) "1引数の場合、それを戻す; 0の場合、デフォルトを戻す。 1つより多くの引数がある場合、それらの上にopをconsする。 例: (maybe-add 'progn '((f x))) ==> (f x) 例: (maybe-add '* '(3 4)) ==> (* 3 4) 例: (maybe-add '+ '()) ==> 0, 0は+に対する同一物として定義されると仮定している。" (cond ((null args) default) ((length=1 args) (first args)) (t (cons op args)))) (deftable identity :init '((+ 0) (* 1) (and t) (or nil) (progn nil))) 4.4. シンタックス抽象 簡約のための言語 仕事: 全ての式に対する簡約 (simplify '(* 1 (+ x (- y y)))) ==> x (simplify '(if (= 0 1) (f x))) ==> nil (simplify '(and a (and (and) b))) ==> (and a b) シンタックス抽象はその問題に適切な新たな言語を定義する。 これは(コード指向とは逆に)問題指向のアプローチである。 簡約のルールのための言語を定義し、それからいくつかを書く: (define-simplifier exp-simplifier ((+ x 0) ==> x) ((+ 0 x) ==> x) ((= x 0) ==> x) ((- x x) ==> 0) ((if t x y) ==> x) ((if nil x y) ==> y) ((if x y y) ==> y) ((and) ==> t) ((and x) ==> x) ((and x x) ==> x) ((and t x) ==> x) ...) 注意深くあなたの言語を設計せよ "記法を変更する能力は人類に力を与える。" - Scott Kim 悪い: 冗長、脆い (setq times0-rule '( simplify (* (? e1) 0) 0 times0-rule ) ) (setq rules (list times0-rule ...)) - 不十分な抽象化 - 三回times0-ruleを名付けることを必要とする - 不要なグローバル変数を導入している - 慣習的ではないインデント ときどき、ルールを名付けることが有益である: (defrule times0-rule (* ?x 0) ==> 0) (この場合は勧めないだろうが) 簡約化のためのインタプリタ 現在ではインタプリタ(またはコンパイラ)を書ける: (defun simplify (exp) "まずコンポーネントを簡約化することによって式を簡約化する。" (if (atom exp) exp (simplify-exp (mapcar #'simplify exp)))) (defun-memo simplify-exp (exp) "ルール、または数学を用いて式を簡約化する。" ;; 式はアトムではない。 (rule-based-translator exp *simplification-rules* :rule-pattern #'first :rule-response #'third :action #'simplify :otherwise #'eval-exp)) この解決法は良い。なぜなら: - 簡約化の規則は書きやすい - 制御の流れが(ほとんど)抽象化し去られている - ルールが正しいことの検証が容易である - プログラムは素早く立ち上げて実行できる。 そのアプローチが十分である場合、それで終わりである。 そのアプローチが不十分である場合、時間を節約した。 それが単に遅い場合、ツールを改善でき、 そのツールの他の使用も恩恵を受けるだろう。 変換のためのインタプリタ "成功は何度も何度も同じことを行なうことからやってくる; それぞれのとき に少しだけ学び、次回は少しだけより良く行なうのである。" - Jonathan Sachs ルールに基づくトランスレータを抽象化し尽くす (defun rule-based-translator (input rules &key (matcher #'pat-match) (rule-pattern #'first) (rule-response #'rest) (action #'identity) (sub #'sublis) (otherwise #'identity)) "inputにマッチする最初のルールを見つけて、マッチの結果 をルールの応答に置き換えた結果へactionを適用する。マッ チするルールがない場合、inputへotherwiseを適用する。" (loop for rule in rules for result = (funcall matcher (funcall rule-pattern rule) input) when (not (eq result fail)) do (RETURN (funcall action (funcall sub result (funcall rule-response rule)))) finally (RETURN (funcall otherwise input)))) この実装があまりにも遅い場合、もっとうまくインデックス付けしたりコンパ イルしたりできる。 ときどき、再利用は非公式なレベルである; どのように一般的なツールが作ら れているかを理解することは、プログラマがカットアンドペーストを用いるカ スタムツールを作成することを許す。 重複する仕事を減らす: defun-memo 完全に新たな言語を定義するほど極端ではないことは、新たなマクロでLispを 増加させることである。 defun-memoは関数が行なった全ての計算を覚えておくようにさせる。入力/出 力のペアのハッシュテーブルを保持することでこれを行なう。最初の引数が単 に関数名である場合、2つの内の1つが起こる: [1] 正確に1つの引数があり &rest引数ではない場合、その引数の上にeqlテーブルを作る。[2] そうでなけ れば、引数の全体の上にequalテーブルを作る。 (name :test ... :size ... :key-exp ...)でfn-nameを置き換えることもでき る。これは与えられたtestとsizeをもち、key-expによってインデックス付け られたテーブルを作る。そのハッシュテーブルは、clear-memo関数を用いてク リアすることができる。 例: (defun-memo f (x) ;; x上でキー付けられたeqlテーブル (complex-computation x)) (defun-memo (f :test #'eq) (x) ;; x上でキー付けられたeqテーブル (complex-computation x)) (defun-memo g (x y z) ;; (x y . z)上でキー付けられた (another-computation x y z)) ;; equalテーブル (defun-memo (h :key-exp x) (x &optional debug?) ;; x上でキー付けられたeqlテーブル ...) (defmacro defun-memo (fn-name-and-options (&rest args) &body body) ;; 前のページの文書文字列 (let ((vars (arglist-vars args))) (flet ((gen-body (fn-name &key (test '#'equal) size keyexp) `(eval-when (load eval compile) (setf (get ',fn-name 'memoize-table) (make-hash-table :test ,test ,@(when size `(:size ,size)))) (defun ,fn-name ,args (gethash-or-set-default ,key-exp (get ',fn-name 'memoize-table) (progn ,@body)))))) ;; マクロの本体: (cond ((consp fn-name-and-options) ;; 存在する場合、ユーザが供給するキーワードを用いる (apply #'gen-body fn-name-and-options)) ((and (= (length vars) 1) (not (member '&rest args))) ;; 正当であるならeqlテーブルを用いる (gen-body fn-name-and-options :test '#'eql :key-exp (first vars))) (t ; そうでなければ全ての引数にequalテーブルを使う (gen-body fn-name-and-options :test '#'equal :key-exp `(list* ,@vars))))))) マクロをもっと (defmacro with-gensyms (symbols body) "本体のあらゆる場所で、与えられたシンボルをgensymされた バージョンで置き換える。マクロのために有益である。" ;; どこでもこれを行なう。単に"変数"のためではない。 (sublis (mapcar #'(lambda (sym) (cons sym (gensym (string sym)))) symbols) body)) (defmacro gethash-or-set-default (key table default) "テーブルから値を得るか、それをデフォルトに設定する。 必要になるまでそのデフォルトを評価しない。" (with-gensyms (keyvar tabvar val found-p) `(let ((keyvar ,key) (tabvar ,table)) (multiple-value-bind (val found-p) (gethash keyvar tabvar) (if found-p val (setf (gethash keyvar tabvar) ,default)))))) 適切にマクロを用いる (Allan Wechslerによるチュートリアルを参照) マクロのデザイン: - マクロが本当に必要かどうかを決定する - マクロに対して明快で一貫したシンタックスを選ぶ - 正しい展開を理解する - マッピングを実装するためにdefmacroと`を用いる - 多くの場合、関数のインターフェイスも提供する(有益であり、しばしば変 更して継続することがより易しい) 考えるべき事柄: - 関数が十分であるところでマクロを使わないこと - 展開時に何も行なわないことを確実にする(ほとんどの場合) - 左から右へ、それぞれ一度だけ引数を評価する(行なうなら) - ユーザ名と衝突しないこと(with-gensyms) マクロでの問題 悪い: インライン関数であるべき (defmacro name-part-of (rule) `(car ,rule)) 悪い: 関数であるべき (defmacro defpredfun (name evaluation-function) `(push (make-predfun :name ,name :evaluation-function ,evaluation-function) *predicate-functions*)) 悪い: 展開時に動作する (defmacro defclass (name &rest def) (setf (get name 'class) def) ... (list 'quote name)) 悪い: マクロは引数をevalしてはいけない (defmacro add-person (name mother father sex unevaluated-age) (let ((age (eval unevaluated-age))) (list (if (< age 16) ... ...) ...))) (add-person bob joanne jim male (compute-age 1953)) 今この呼び出しをコンパイルし、数年後にそれをロードした場合はどうなるか? より良い: コンパイラに定数畳み込みをさせる (declaim (inline compute-age)) (defmacro add-person (name mother father sex age) `(funcall (if (< ,age 16) ... ...) ...)) とても悪い: (incrementがnだったらどうなるか?) (defmacro for ((variable start end &optional increment) &body body) (if (not (numberp increment)) (setf increment 1)) ...) (for (i 1 10) ...) 制御構造に対するマクロ 良い: CLの直行性の穴を満たす (defmacro dovector ((var vector &key (start 0) end) &body body) "varがvectorのそれぞれの要素に束縛された状態でbodyを行なう。 vectorの範囲の一部を指定できる。" `(block nil (map-vector #'(lambda (,var) ,@body) ,vector :start start :end end))) (defun map-vector (fn vector &key (start 0) end) "ある範囲内のvectorのそれぞれの要素の上でfnを呼び出す。" (loop for i from start below (or end (length vector)) do (funcall fn (aref vector-var index)))) - 普通のデータ型の上を繰り返す - 確立されたシンタックス(dolist, dotimes)に従っている - 宣言と戻り値に従う - キーワードを用いて確立されたシンタックスを拡張している - 一つ悪い点: dolist, dotimesのような結果がない マクロに対するヘルパー関数 多くのマクロは関数への呼び出しへ展開するべきである。 マクロdovector実際の仕事は関数map-vectorによってなされるが、その理由は: - パッチをあてやすい - 独立して呼び出せる(プログラムにとって有益) - 結果的なコードがより小さい - お好みならば、ヘルパー関数はインラインにできる (クロージャをコンスすることを避けるためにしばしばよい) (dovector (x vect) (print x)) は以下へマクロ展開する: (block nil (map-vector #'(lambda (x) (print x)) vect :start 0 :end nil)) これは(おおよそ)以下にインライン展開する: (loop for i from 0 below (length vect) do (print (aref vect i))) Setfメソッド マクロと同様に、正確に一度だけ、左から右への順序でそれぞれのフォームを 評価することを確実にしなければならない。 マクロ展開(macroexpand, get-setf-method)が正しい環境で行なわれることを 確実にすること。 (defmacro deletef (item sequence &rest keys &environment environment) "破壊的にsequenceからitemを削除する。" (multiple-value-bind (temps vals stores store-form access-form) (get-setf-method sequence environment) (assert (= (length stores) 1)) (let ((item-var (gensym "ITEM"))) `(let* ((,item-var ,item) ,@(mapcar #'list temps vals) (,(first stores) (delete ,item-var ,access-form ,@keys))) ,store-form)))) 5. 大規模なプログラミング 大規模なプログラミング ソフトウェア開発の段階を理解すること: - 要求収集 - アーキテクチャ - コンポーネント設計 - 実装 - デバッグ - チューニング これらは重なり得る。探検的プログラミングのポイントはコンポーネント設計 の時間を最小化し、アーキテクチャや要求が正しいかどうかを決定するために 素早く実装に移行することである。 どのように大きなプログラムをまとめるかを知ること: - パッケージを用いる - defsystemを用いる - ファイルにソースコードを分割する - 大規模な文書 - 移植性 - エラーの扱い - Lisp以外のプログラムとのインターフェイス ファイルにソースコードを分割する 以下の要因がどのようにコードがファイルに分解されるかに影響する - 言語が強制する依存性 使用前のマクロ、インライン関数、CLOSクラス - 階層化設計 再利用可能なコンポーネントを分離する - 機能分解 コンポーネントと関連するグループ - ツールとの互換性 エディタやcompile-fileにとってよいサイズのファイルを選ぶ - OS/マシン/ベンダ特有の実装を分離する 効果的にコメントを用いる 以下のためにコメントを用いる: - 哲学を説明する。単に詳細を文書化しないこと; 同様に、コードの全体構造 を理解するためのフレームワークを提供する哲学、動機、そしてメタファー を文書化すること。 - 例を提供する。しばしば、一つの例はひと山の文書と同じ価値がある。 - 他の開発者の対話をもつこと! 共同プロジェクトでは、しばしばソースの中 に単に置くだけで尋ねることができる。それが答えられているのを見つける ために戻ってきてもよい。同様に、後で疑問に思うかもしれない他の人のた めに疑問と回答を置いておくこと。 - あなたの"to do"リストを維持すること。後で戻ってきたいコメントに特別 な目印を置く: ???または!!!; より高い優先度に対して!!!!を使ってもよい。 ソースコードとは別のファイルにto doリストや変更ログを保存するプロジェ クトもある。 (defun factorial (n) ;; !!! 負の数についてはどうなるか? --Joe 03-Aug-93 ;; !!! それから数以外についてはどうなるか?? -Bill 08-Aug-93 (if (= n 0) 1 (* n (factorial (- n 1))))) 文書: 意図していることを言え Q: コードを書くときに使ったことがコメントをありますか? "手続きの先頭やデータ構造だけにコメントをつけるが、それらを除いてめっ たにない。コード自身にはコメントをつけないが、それは正しく書かれたコー ドはまさにそれ自身が文書であると感じているからである。" - Gary Kildall "私は二つのタイプのコメントがあると考える: 一つは明らかなことを説明し ているもので、それらは価値がないというよりも悪いものである。他の種類は 真に複雑な、入り組んだコードを説明するときのものである。ところで、私は 入り組んだコードをいつも避けようとしている。余分な五行が必要だとしても、 私は真に強固な、明白な、きれいなコードをプログラムしようとする。私はほ ぼ、コメントが必要であればあるほど、あなたのプログラムは悪くて何かが間 違っているという意見である。" - Wayne Ratliff "悪いコメントにコメントをつけるな - 書き直せ。" - Kernighan & Plauger - システムの目的と構造を記述する - それぞれのファイルを記述する - それぞれのパッケージを記述する - 全ての関数に対する文書文字列 - 自動的なツール(マニュアル)を考慮する - コードを作ること。コメントを作るのではない 文書化; 過剰コメント これらの32行は、ある主要なシステムを文書化しているに違いない: ; ====================================================================== ; ; describe ; -------- ; ; arguments : snepsul-exp - ; ; returns : ; ; description : This calls "sneval" to evaluate "snepsul-exp" to ; get the desired . ; It prints the descriptions of each in the ; that has not yet been described during ; the process; the description includes the ; description of all s dominated by the . ; It returns the . ; ; implementation: Stores the s which have already been described ; in "describe-nodes". ; Before tracing the description of a , it ; checks whether the was already been described ; to avoid describing the same repeatedly. ; The variable "describe-nodes" is updated by "des1". ; ; side-effects : Prints the 's descriptions. ; ; written: CCC 07/28/83 ; modified: CCC 09/26/83 ; ejm 10/10/83 ; njm 09/28/88 ; njm 4/27/89 (defmacro describe (&rest snepsul-exp) `(let* ((crntct (processcontextdescr ',snepsul-exp)) (ns (in-context.ns (nseval (getsndescr ',snepsul-exp)) crntct)) (described-nodes (new.ns)) (full nil)) (declare (special crntct described-nodes full)) (terpri) (mapc #'(lambda (n) (if (not (ismemb.ns n described-nodes)) (PP-nodetree (des1 n)))) ns) (terpri) (values ns crntct))) 問題点: - 文書が長過ぎる: 大局を見失っている - 文書が間違っている: describe(d)-nodes - 文書が効果的ではない: 文書文字列がない - 文書が冗長である: (arglist) - Lispのdescribe関数をシャドウするのは悪いアイディアである - マクロから分離する関数が必要である - 短縮が不明瞭である 文書化: コメント より良い: これは(それが何であれ)crntctを扱わない (defmacro desc (&rest snepsul-exp) "この式から参照されるノードを記述する。 このマクロは対話的なデバッグツールとして意図している; プログラムからは関数describe-node-setを用いること。" `(describe-node-set (exp->node-set ',snepsul-exp))) (defun describe-node-set (node-set) "このノードの集合の中の全ノードを印字する。" ;; 重複を取り除くためにdescribed-nodesを蓄積する。 (let ((described-nodes (new-node-set))) (terpri) (dolist (node node-set) (unless (is-member-node-set node described-nodes) ;; des1はdescribed-nodesへノードを加える (pp-nodetree (des1 node described-nodes)))) (terpri) node-set)) 移植性 あなたが使っている環境でプログラムをうまく動作するようにすること。 しかし、あなたか誰か他の人がいつか別の環境でそれを使いたいかもしれない ということを知っておくこと。 - #+featureと#-featureを使う - 実装依存の部分を分離する - 一つのソースと複数のバイナリを保守する - dpANS CLの方向へ進化すること 必要ならば欠けている機能を実装すること - ベンダ特有の拡張を知ること 外部関数インターフェイス 大規模なプログラムは、しばしば他の言語で書かれた他のプログラムとのイン ターフェイスをもたなければならない。残念なことに、これに対する標準はな い。 - ベンダの外部インターフェイスを学ぶ - データの交換を最少化する - 問題を引き起こす領域を知る メモリ管理 シグナルハンドリング 6. その他 意図することを言え - 読者を誤解させるな 読者の誤解を予期すること - 正しいレベルの特定性を用いる - 宣言に注意する 不正な宣言はコードを壊す - 一対一の対応 悪い宣言: 例のためにのみ作られた (defun lookup (name) (declare (type string name)) (if (null name) nil (or (gethash name *symbol-table*) (make-symbol-entry name)))) (declare (type (or string null) name))であるべき 命名規約: 一貫して 名前において一貫すること: - 大文字小文字の使い方を一貫すること 多くの人はLikeThisよりもlike-thisを好む - *special-variable* - +constant+ (あるいは何らかの規約) - Dylanはを用いる - structure.slotを考慮する - -pまたは?; !またはn; ->または-to- 動詞-オブジェクト: delete-file オブジェクト-属性: integer-length name-fileとfile-nameを比べること オブジェクト-動詞または属性-オブジェクトを使わないこと! - 引数の順序を一貫する - 内部的な関数と外部的な関数を区別する &optionalと&keyを混ぜないこと; 1つか2つの&optional引数を注意深く使う こと(Dylanは0である) 一貫してキーワードを用いる(key, test, end) 命名規約: 賢明に名前を選ぶ 賢明に名前を選ぶ: - 短縮を最少にする ほとんどの単語は多くの短縮の可能性があるが、正しい綴りは一つだけであ る。読みやすく、覚えやすく、見つけやすくなるように、名前を完全に綴る こと。 いくつかの可能な例外: char, demo, intro, そしてparen。これらの単語は ほとんど真の英単語のようになって来ている。(英語のネイティブにとって) よいテストは: 会話の中でその単語を声をだして言うことができるか? crntctやprocesscontextdescrを用いた我々の前の例はこのテストをパスし ないだろう。 - 別のローカル変数でローカル変数をシャドウしないこと。 - 更新される変数をはっきりと示すこと。 - 曖昧な名前を避ける; lastではなくpreviousまたはfinalを用いる。 記法上のトリック: 0カラム上の括弧 多くのテキストエディタは、トップレベルの式の開始としてカラム0の左括弧 を扱う。カラム0にある文字列の中の括弧は、バックスラッシュを供給しない とエディタを混乱させるかもしれない: (defun factorial (n) "整数の階乗を計算する。 \(整数ではない引数については気にするな)." (if (= n 0) 1 (* n (factorial (- n 1))))) 多くのテキストエディタはカラム0にある"(def"を定義として扱うが、他のカ ラムにある"(def"は定義としては扱わないだろう。だから、以下のようにする 必要があるかもしれない: (progn (defun foo ...) (defun bar ...) ) 複数行の文字列 以下のようなリテラル定数としての複数行の文字列の場合: (defun find-subject-line (message-header-string) (search " Subject:" message-header-string)) 代わりに読み込み時の評価とformatへの呼び出しの使用を考慮すること: (defun find-subject-line (message-header-string) (search #.(format nil "~%Subject:") message-header-string)) 同じ文字列が何度も使われるところでは、グローバル変数または名前づけられ た定数の使用を考慮すること: (defparameter *subject-marker* (format nil "~%Subject:")) (defun find-subject-line (message-header-string) (search *subject-marker* message-header-string)) 長いformat文字列に対しては、または@を用いて継続行をイ ンデントすることができる。以下の二つのフォームは同じことを行なう: (format t "~&This is a long string.~@ This is more of that string.") This is a long string. This is more of that string. (format t"~&This is a long string.~ ~%This is more of that string.") This is a long string. This is more of that string. 後者のシンタックスは、決まった量だけ容易にインデントできるようにする: (format t "~&This is a long string.~ ~% This is more of that string, indented by one.") This is a long string. This is more of that string, indented by one. 記法上のトリック: 複数行のコメント 文字列の中で#|や|#の使用を避けること。なぜなら、そのような文字列をコメ ントアウトしようとするあらゆる後の試みを混乱させるからである。再び、バッ クスラッシュが助けになる: 良い: (defun begin-comment () (write-string "#\|")) (defun end-comment () (write-string "|\#")) これは、文字列自身を編集することなしに、これらの文字列を含んでいるセク ションを後にコメントアウトできることを意味する。 あなたのエディタがサポート(comment-regionやuncomment-regionコマンド)を 提供する場合、明示的な;;コメントを用いるほうが良い。その方法だと、リー ダはどの部分がコメントアウトされたかについて混乱することはないだろう。 いくつかの赤旗 以下の状況は"赤旗"である。それらはしばしば問題の兆候である - 技術的に は、それらの多くは完全に正しい状況でも同様に発生するのだが。これらの赤 旗の一つを見た場合、コードの中に問題があることに自動的になるわけではな いが、それでも注意深く進むべきである: - あらゆるevalの使用 - あらゆるgentempの使用 * - あらゆるappendの使用 - setfを用いるか、macroexpandを呼び出すマクロに&environmentパラメータ がないこと - 型errorに対してコンディションハンドラを書くこと(ignore-errorsの使用 を含む) - caar, cad...rを除くあらゆるc...r関数の使用("..."は全てd) * 良い使用方法は知られていない。 ありふれた間違いを避ける 良いスタイルは間違いを避けることを伴う。 - 常に入力を促す (そうでなければ、ユーザは何が起こっているかを知らない) - defvarとdefparameterを理解する - fletとlabelsを理解する - 多値を理解する - (上に示したように)マクロを理解する - マクロまたはインライン関数を変更した後に再コンパイルする - '(lambda ...)ではなく#'(lambda ...)を用いる - #'fは単に(function f)であることを忘れない - 必要なら:test #'equalを用いる - 宣言が効果があることを確実にする - 破壊的な関数に対してポリシーをもつ 破壊的な関数 破壊的な関数に対してポリシーをもつ: - 多くのプログラムは、引数が(関数のnconcが部分的に生じるときと同様に) 他の場所で必要とされないと証明できるときに破壊的な更新を用いる。 - そうでなければ、その引数は変更できないと仮定すること。 - 結果は変更されないと仮定すること。 - 主要なインターフェイスは、単に安全性のために、しばしば渡す結果のコピ ーを作る。 - ジェネレーションスキャベンジングGCは、破壊的な更新によって遅くなり得 ることに注意すること。 あまり重要ではない過ち 悪い: (defun combine-indep-lambdas (arc-exp) (apply #'* (mapcar #'eval-arc-exp (cdr arc-exp)))) - applyはcall-arguments-limitを超えるかもしれない - mapcarはごみを生成する - cdrはデータ抽象を破壊する 良い: (reduce #'* (in-arcs arc-exp) :key #'eval-arc-exp) アキュムレータの使い方を学ぶ: (defun product (numbers &optional (key #'identity) (accum 1)) "(reduce #'* numbers)に似ているが、 ゼロが見つかった時点で早く脱出する。" (if (null numbers) accum (let ((term (funcall key (first umbers)))) (if (= term 0) 0 (product (rest numbers) key (* accum term)))))) シリーズを考慮する: (collect-fn 'number (constantly 1) #'* numbers) マルチタスクとマルチプロセス マルチタスク (タイムスライス) マルチタスクにも関わらずうまく動作するようにコードを組み立てるのに時間 を費やすことは合理的である。移植可能な標準はまだないが、多くの商用の Lisp実装はこれをもつ。それは既存の言語のセマンティックスとうまく合致し ている。 - setqや属性リストのようなグローバルな状態に注意すること。 - without-interrupts, without-aborts, without-preemptionなどを用いてプ ロセスを同期すること。利用可能なオペレータの集合については実装固有の 文書を調べ、それらがどのように異なるかを学ぶこと。 マルチプロセス (真の並列性) 真の並列性について考えること。しかし、物事が突然並列化された場合にプロ グラムをうまく動作するように構築することに時間を浪費しないこと。逐次的 なプログラムを並列的にすることは、偶然には起こり得ない(たとえば、いく つかのCommon Lispのセマンティックスの一夜のうちの変更による)ような些細 ではない変更である。これをサポートするためには完全に新たな言語が必要で ある; 準備のための時間がいるだろう。 予期せぬことを予期する マーフィーの法則 "何かが悪くなる可能性があるなら、そうなるだろう。" 非常に確信していないかぎり、何かがけっして起こらないことを確信している のだから、物事のチェックを省略しないこと。…そして、非常に確信していて も、いずれにせよ省略しないこと。"これは起こり得ない"というシステムから のエラーを得ることは十分にありふれており、人々は自分たちが考えるほど常 に明晰ではないことは明らかである。 他の人々のコードを読む "他の人々の仕事を学ぶ必要がある。その人々の問題解決に対するアプローチ や用いるツールは自身の仕事を見るための新鮮な方法論を与える。" - Gary Kildall "私は他の人々のプログラムを見ることから多くのことを学んだ。" - Jonathan Sachs "私は、プログラミング能力のもっともよいテストの一つは、だいたい30ペー ジのコードをプログラマに渡して、どのくらい素早く全体を読んで理解できる かを見ることだと今でも考えている。" - Bill Gates "[プログラマになるために]準備する最良の方法は、プログラムを書くことと、 他の人々が書いた偉大なプログラムを学ぶことである。私の場合、計算機科学 センターのゴミ箱のところへ行ってオペレーティングシステムのリストを探り 出した。" - Bill Gates "喜んで他の人々のコードを読み、あなた自身のコードを書き、他の人々にあ なたのコードを査閲してもらわなければならない。" - Bill Gates - Lisp Machineオペレーティングシステム - インターネットFTPサイト(comp.lang.lisp FAQ) - CMU CLコンパイラやユーティリティ - Macintosh Common Lispの例 例: deftable 仕事: テーブルの定義と使用を容易にする。 - defstructのように - 速くなければならない: インライン関数 - 一つまたは複数のテーブルを扱うべきである - CLOSは? - 操作は? 引数と戻り値は? - デフォルト値は? 変更される? それとも戻される? ユーザと実装者のコードを分離し、両方をサポートする。 - 新たなテーブルの実装を定義するための方法 - 名前づけ; パッケージは? - 文書化される制限は? - 処置(Instrumentation)は? - 自動的な選択は? 学んだ教訓: - 普通の抽象構造を捉える: テーブル、その他は? - 複雑なマクロは多少の注意をもって設計し得る - 拡張の可能性を考慮する プロトタイプ Lispは容易にプロトタイプを開発できるようにする。 "一つは捨て去るように計画すること; いずれにせよそうなるのだから。" - Fred Brooks "私は何かを行なう前には大いに考え、一度何かを行なうと、それを捨て去る ことを恐れない。プログラマが本の中の悪い章のようにコードを振り返って見 て、尻込みすることなく捨てることができるのは重要である" - John Warnock "早い時期に固定してはいけない; 必要以上に早く決定してはいけない。大き さの次数を必要だと考えるよりも一般的なものにとどめておくこと。なぜなら、 結局長い目で見れば必要になるからだ。とても素早く何かを動作させ、それか ら、それを捨てられるようにすること。" - John Warnock "だから、私はあるときに数行を書き、それを十分に試し、動作するようにし て、それからさらに数行を書く傾向がある。私は、真に多数の変更を行なうた めには、繰り返しごとに最少の量の仕事をしようとする。" - Wayne Ratliff "1-2-3は動いているプログラムとして始まり、その開発の間ずっと動いている プログラムであり続けた。" - Jonathan Sachs その他のアイディア タイプの打ち方を学ぶこと。60語/分以下しかタイプできない場合、あなたは 自分自身を押えつけているのである。 "また、複雑なプログラム上で熱心に働いているときには、運動をすることが 重要である。運動不足には多くのプログラマが陥る。それは精神的な鋭敏さの 欠乏を引き起こす。" - John Page Q: 偉大なプログラマになるためには何が必要でしょうか? "何かを上手になるためには何が必要だろうか? 良い作家になるには何が必要 だろうか? 上手な人は二つの要素の組み合わせである: 訓練の必要性への偶然 の精神的な一致と、愚かではいまいとする精神的な能力だ。それは稀な組み合 わせだが、神秘的なものは何もない。良いプログラマは間違いなくプログラミ ングを楽しんでおりプログラミングに関心があるため、より多くのことを学ぼ うとするだろう。良いプログラマは複雑さに罪の意識をもつことと結び付いた 美的感覚と、その美的感覚をいつ破壊するかに関する鋭い知覚も必要とする。 複雑さへの罪の意識は、プログラムを改善するために、そして美的感覚を用い てそれを整列するために、より懸命に働くことを彼に強いるのである。" - Bob Frankston 推薦する書籍 Common Lispへの入門 - Robert Wilensky Common LISPcraft - Deborah G. Tatar A Programmer's Guide to Common Lisp - Rodney A. Brooks. Programming in Common Lisp 参考文献と必須文献 - Guy L. Steele Common Lisp: The Language, 2nd Edition - ANSI Draft Proposed Common Lisp Standard - Harold Abelson and Gerald Jay Sussman, with Julie Sussman. Structure and Interpretation of Computer Programs (Scheme) より高度なもの: - Patrick H. Winston and Berthold K. P. Horn. LISP, 3rd edition. - Wade L. Hennessey Common Lisp - Sonya E. Keene Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS - Eugene Charniak, Christopher K. Riesbeck, Drew V. McDermott and James R. Meehan. Artificial Intelligence Programming, 2nd edition. - Peter Norvig. Paradigms of AI Programming: Case Studies in Common Lisp 定期刊行物: - LISP Pointers. (ACM SIGPLAN) Since 1987. - LISP and Symbolic Computation. Since 1989. - Proceedings of the biannual ACM Lisp and Functional Programming Conference. Since 1980. 引用 Programmers at Work, Susan Lammers, Microsoft Press, 1989. からの引用 - Bob Frankston: Software Arts VisiCalc; Lotus - Bill Gates: Altair BASIC; Microsoft - Gary Kildall: Digital Research CP/M - Scott Kim: Stanford, Xerox; Inversions - Butler Lampson: Xerox Ethernet, Alto, Dorado, Star, Mesa; DEC - John Page: HP; Software Publishing PFS:FILE - Wayne Ratliff: NASA; Ashton-Tate dBASE II - Charles Simonyi: Xerox Bravo; Microsoft Word, Excel - John Warnock: NASA; Xerox; Adobe PostScript その他の引用: - Harold Abelson: MIT; SICP; Logo - Judy Anderson: Harlequin, Inc. - Fred Brooks: IBM 360 architect, now at UNC - Bear bryant: Alabama football coach - Brian Kernighan & P.J. Plauger: Bell Labs UNIX - David McDonald: MIT, UMass natural language generation - Guy Steele: Thinking Machines; Scheme; CLtL - Gerald Sussman: MIT; SICP; Scheme - Deborah Tatar: DEC; Xerox; author - Niklaus Wirth: ETH Zurich; Pascal ほとんど全ての悪いコードの例は、出版された本や記事からとられている(そ れらの作者はもっと良くわかっているだろうが、匿名のままにしておこう)。 Local Variables: version-control: t kept-old-versions: 0 kept-new-versions: 0 end: