–
第4章
–
関数とプログラム構造
第4章
関数とプログラム構造
本章の冒頭には、C 言語が ANSI 標準規格に対応し、関数を宣言するときに引数の型を宣言することが出来るようになり、 コンパイラがより多くのエラーを検出出来るようになった、ということが書かれている。 これが出来るようになる前は、lint コマンドにより引数の型のゆらぎをチェックするにとどまっていた。lint チェッ クの難点は、引数の型や個数に揺らぎがあると、その中のどれが正しいかが分からないために、非常に多くのワーニン グを出力してしまうことであり、これを一つ一つチェックするのがかなりたいへんだった。ひと頃(1990 年代前半)、 Sun Workstation を開発環境とした開発をやっていて、SunOS 4.1.3 以前は標準の cc コマンドが ANSI 対応前のも のであり、gcc をインストールして関数のプロトタイプチェックをやっていた記憶がある。開発リーダは、このプロト タイプチェックを完璧に行なえるツールを作って欲しいと言っていたこともあった。その後、SunOS は BSD から SystemV 系 UNIX に変わり、カーネルのリコンパイルという概念が無くなり、C コンパイラは標準ツールから外れてし まった。俗にスパコンと呼ばれていた SPARCompiler は ANSI 規格準拠になったが、なぜかこのスパコンを使ったとい う記憶はほとんどない。 今は自宅のパソコンで普通に UNIX(Linux や BSD)が動いて、GNU のソフトウェアは何でも使いたい放題という、上記 の頃からすれば、夢のような状況になっている。しかも、当時は UNIX 上で Emacs や gcc を使いながら、ワープロで文 書を書くなんてことが出来なくて、こういった文書を作りたければ TeX で書く以外に方法がなかった...といったこと を懐かしく思い出しながら、この章を書くことになりそうだ。4.1
4.1 関数についての基本事項
関数についての基本事項
ここまでにたくさんの関数を書いてきたので、いまさら基本事項なのかという気もするが、elisp に関しては、まだ関 数についてきちんとした説明はしてきていない。 (lambda (x) "Return the hyperbolic cosine of X." (* 0.5 (+ (exp x) (exp ( x))))) これが Info に最初に出てくる lambda 式のサンプルであり、 (lambda (ARGVARIABLES…) [DOCUMENTATIONSTRING] [INTERACTIVEDECLARATION] BODYFORMS…) これが lambda 式(ラムダ式)の完全なスペックであるが、これを示されても、すぐに腹に落ちる人は少ないだろう。 私もこの lambda というキーワードを知ってから、これがおぼろげに分かるのに 10 年くらいはかかっている(もちろん 10 年間 elisp に掛かりきりだったわけではない、念のため)。逆に言えば、lambda が何なのかを理解出来ていなくて も、ある程度 elisp で関数を書いたり、コマンド書いたりすることが出来る。 ───────────────────────────────────────────────────── #!/usr/local/bin/emacs script (funcall (lambda () (princ "hello, world\n"))) $ ./lambdahw.elhello, world
───────────────────────────────────────────────────── lambda 式を実行するには funcall(または apply)を使用する。以降、Emacs の Info を見ながら*scratch*バッ ファであれこれ試してみる。 (lambda (a b c) (+ a b c))^J (lambda (a b c) (+ a b c)) lambda 式を評価するとそのまま返される。少なくとも lambda が何らかのファンクションのような扱いになっているこ とが分かる。そうでなければ、lambda などというファンクションは無い、というエラーになるはず。 (quote (lambda (a b c) (+ a b c)))^J (lambda (a b c) (+ a b c)) lambda 式を quote しても、結果は同じである。 (funcall (lambda (a b c) (+ a b c)) 1 2 3)^J 6 引数を指定して lambda 式を実行するには funcall を使用する。 (funcall (lambda (a b c) (+ a b c)) 1 (* 2 3) ( 5 4))^J 8 引数は評価結果が lambda 式に渡される。つまり、1、6、1 の 3 つが引数になる。 (REQUIREDVARS… [&optional OPTIONALVARS…] [&rest RESTVAR]) これは引数リストの完全なスペックである。REQUIREDVARS がある場合は、これを必ず渡す必要がある。C 言語の場合 は、すべてがこの REQUIREDVARS であると言える。二つ目の OPTIONALVARS は、&optional というキーワードを 伴って、その名のとおり必要があれば指定する、省略可能な引数である。三つめの RESTVAR は、OPTIONALVARS と 同様に省略可能だが、引数をいくつでも受けとることが出来る。また、この三種類の引数は必ずこの順番で指定されな ければならない。省略された場合は nil を受け取る。 (funcall (lambda (n) (1+ n)) 1)^J 2 (funcall (lambda (n &optional n1) (if n1 (+ n n1) (1+ n))) 1 2)^J 3 (funcall (lambda (n &rest ns) (+ n (apply '+ ns))) 1 2 3 4 5)^J 15 おそらく C プログラマにとって馴染み難いのが、三つ目の&rest ではないだろうか。上の例では、1、2、3、4、5 の 5 個の引数を渡しているが、これを(n &rest ns)の通りに受けとると、(1 (2 3 4 5 6))という意味になる。つまり n が 1 で、ns が(2 3 4 5)というリストになるというわけだ。
(funcall (lambda (n &rest ns) (list n ns)) 1 2 3 4 5)^J (1 (2 3 4 5)) 当たり前だが、こうすれば&optional や&rest がどのように引数を受けとるか、確認することが出来る。 さて、原著では本節で grep コマンドもどきを作ろうとしているようだ。 Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits and then Remould it nearer to the Heart's Desire! 上のテキストから"ould”を含む行を見つけると、次の行が得られる。 Ah Love! could you and I with Fate conspire Would not we shatter it to bits and then Remould it nearer to the Heart's Desire! ───────────────────────────────────────────────────── #!/usr/local/bin/emacs script (require 'stdio) (require 'cl)
(defconst greppattern "ould")
(loop for s = (getline) until (eq s 'EOF) when (stringmatch greppattern s) do (printf "%s\n" s)) $ ./greph.el < greph.txt Ah Love! could you and I with Fate conspire Would not we shatter it to bits and then Remould it nearer to the Heart's Desire! ───────────────────────────────────────────────────── C 言語の方は、main、getline、strindex という 3 つの関数で書かれているが、getline を stdio.el に移したの で、残りカスのようなプログラムになってしまった。1 文字毎に処理するプログラムは、以前の章でたくさん書いたので、 その必然性が感じられないものは繰り返さない。そうしないと elisp らしいプログラムにならないからである。
ところで、C 言語では関数のデータ型をきっちり定義し、その型のデータを return していないと、コンパイラがエラー やワーニングを出す。elisp にも return はあるが、これは対応するタグに throw するものであり、C 言語のリターン とは根本的に違うものである。そして elisp の場合は、関数の中で最後に評価した式の値が必ず返される。 複数のファイルにまたがった elisp のプログラムをロードする方法は、上の例においても既に stdio という標準入出力 ライブラリもどきをロードして使用しているが、load 関数により強制的にロードしたり、require 関数によりロード したり、autoload 関数によりファンクション使用時に自動的にロードするよう設定したり、という 3 つの方法がある。 たとえば、stdio を require する代わりに (load "stdio" t t) こう書いても同じ動作になる。とくにscript オプションによるバッチ処理の場合は、必ずロードする必要があるの
で、load でも require でも動作的には変わらない。require によりロードする意味があるのは、既にロードされてい る可能性がある場合だけである。また、elisp ファイルをロードする場合は、loadpath リストに登録されているパス にあるファイルが対象になる。
演習
演習
41
41
s における t の最も右側の出現位置を返す関数 strrindex(s, t)を書け。 ───────────────────────────────────────────────────── #!/usr/local/bin/emacs script (require 'stdio) (require 'cl)(defconst greppattern "ould") (defun strrindex (s) (and (stringmatch greppattern s) (1 (matchend 0)))) (loop for s = (getline) until (eq s 'EOF) for end = (strrindex s) when end do (printf "%d\t%s\n" end s)) $ cat greph.txt | ./greph2.el 13 Ah Love! could you and I with Fate conspire 4 Would not we shatter it to bits and then 7 Remould it nearer to the Heart's Desire! ───────────────────────────────────────────────────── 原著の strindex は、パターンが見つかったときにその先頭位置を返し、見つからなかった場合は1 を返す仕様になっ ていた。演習課題の strrindex も、末尾位置を返す以外は同様である。elisp の方は、パターンにマッチしなかった 場合は nil を返し、マッチした場合は末尾位置を返す。処理結果の返し方としては elisp の方が明確であり、受け取っ た方も処理しやすい。これは C 言語における処理結果の判断が数値によるもの(0 かそれ以外か)になるという、根本的 な性質によるものである。これはアセンブラ(機械語)の性質をそのまま受け継いでいるためである。
4.2
4.2
非整数を返す関数
非整数を返す関数
本節のタイトルを見ると、整数でない値を返すことが、あたかも通常でないかのように思える。おそらく C 言語では、 関数の返り値は CPU レジスタで引き継ぐことを前提に、返り値は整数値に決まっていると想定していたのではないか。 これも、アセンブラから受け継いでいるものの一つかもしれない。elisp は prog1 や prog2 のような特殊な逐次制御系のマクロでない限り、最後に評価した式の結果を返す。それが整 数なのか文字列なのかリストなのか、あるいはあらかじめ用意されていない導出タイプなのか、どんなデータであれ、 呼び出し側が受け止めなければならない。もちろん、返り値を無視することは可能だが、基本的に処理結果を返さない (関数の最後に評価する式が処理結果になっていない)関数を書くべきではない。すなわち、C 言語でいう void 型の関 数というのは推奨されない。ただし while のように、必然的に最後に評価した式が nil とならざるを得ないファンク ションも無いわけではない。
number という便利な関数があるので、atof の elisp 版、および演習 42 は省略する。 (stringtonumber "12345678901234567890")^J 1.2345678901234567e+19 ───────────────────────────────────────────────────── #!/usr/local/bin/emacs script (require 'stdio) (require 'cl) (let ((sl (loop for s = (getline) until (eq 'EOF s) collect s))) (princ (format "%s\n" (calceval (mapconcat 'concat sl "+"))))) $ cat primarycalc.txt 0.1234 1.234 12.34 123.4 1234.0 $ ./primarycalc.el < primarycalc.txt 1371.0974 ───────────────────────────────────────────────────── これは原著に載っている「原始的な電卓プログラム」である。C 言語の方では、getline で一行ずつ読みながら、atof により数値化し、累計して表示しているが、elisp の方では途中経過抜きで一発で合計を表示している。まず、 getline を使って取ってきた文字列をすべてリストに格納し、mapconcat を使って文字列+文字列+...+文字列という ふうに連結し、それを calceval に渡して計算させた結果を表示している。 C 言語が関数のデータ型や、引数のデータ型に神経質になるのは、データの型を尋ねることが出来ないからである。なの で、関数の返り値も関数の引数もデータ型が合っていなければ、処理が成り立たなくなる。elisp の場合は、いつもは 整数を返すけど、たまに型が違う nil が返ることもあるよ...といったユルいインタフェースにすることが可能になる。 その場合は、返り値を受け取ってから、何が返ってきたかを確認し処理することになる。 上のプログラムでも、文字列が返ったり、EOF(シンボル)が返ったりするが、何も問題なく処理出来ている。
4.3
4.3
外部変数
外部変数
前にも書いたかもしれないが、プログラミングを覚えて間もないプログラマは、変数はすべて外部変数にしたいと思う かもしれない。これはプログラミングスキルがどんどん向上していけば自然と無くなるし、それどころか外部変数なん て気持ちが悪いので、必要がなければ使わないという感覚に変わってしまう。慣れないうちは、頭の中にトップレベル のデータフローイメージがないので、どれが外部変数であるべきで、どれが内部変数で良いのか、といったことを考え るための基準がない。また、データのフローを明確にイメージ出来れば、外部変数などほとんど不要なのかもしれない。 elisp においては、宣言したり、定義したりした変数以外使用できないなどという制約はないので、あると思って参照 した変数が実はなかったり、自分しか知らない変数だと思って使用した変数が実は存在していて、他の処理に影響して しまう、といったことが起こり得る。そういうことから、私は elisp のスクリプト(バッチ処理)でも、変数が必要に なる場合は let のローカルバインディングを使用する。 さて、逆ポーランド電卓というヘンテコな電卓プログラムが本節のメインテーマのようだが、これを書くのになぜ外部変数が必要なのかは不明である。とりあえず書いてみると...。
─────────────────────────────────────────────────────
#!/usr/local/bin/emacs script (let (stack)
(while (not (string= "Q" (setq input (readstring "INPUT=> ")))) (cond ((stringmatch "^[09.]+$" input) (push (stringtonumber input) stack)) ((string= "+" input) (push (+ (pop stack) (pop stack)) stack)) ((string= "*" input) (push (* (pop stack) (pop stack)) stack)) ((string= "" input) (let ((p (pop stack))) (push ( (pop stack) p) stack))) ((string= "/" input) (let ((p (pop stack))) (if (or (equal 0 p) (equal 0.0 p)) (princ "error: zero divisor\n") (push (/ (pop stack) p) stack)))) ((string= "" input) (let ((p (pop stack))) (if p (princ (format "%f\n" p)) (princ "error: empty stack\n")))) (t
(princ (format "error: unknown command %s\n" input)))))) $ ./pcalc.el INPUT=> 1 INPUT=> 2 INPUT=> INPUT=> 4 INPUT=> 5 INPUT=> + INPUT=> * INPUT=> 9.000000 INPUT=> error: empty stack INPUT=> Q ───────────────────────────────────────────────────── よく考えたら、これは Emacs の GNU 計算機と同じ操作だということに気が付いた。原著の C プログラムだと、入力の読 み取りに getch を使っているため、何を入力したのかエコーされず、使いにくいような気がする。なので、上の elisp 版では少し仕様を変えている。ちなみに、使いにくいような気がするというのは言い訳で、実際には elisp で getch の ような関数は、stdio もどきの小細工では作りようがない。もちろん、Emacs 上のコマンドプログラムとして書けば可 能ではあるが...それだと GNU 計算機と変わらないものになる。
C 言語版の getop は、readstring で十分と考え、省略した。また、スタック操作の push と pop も elisp のビルト インが使えたので作っていない。また、スタックは let を使ってローカルバインドにしたので、結果的に外部変数は使 用する必要がなかった。
これが GNU 計算機の画面である。上の elisp プログラムと同じ結果になるようである。 このあと、演習問題が立て続けに 8 個並んでいるが、演習 47 から 49 は ungetch、つまり数字に続けて+*/を入力 した場合に、次のターンで処理するための退避機能なので扱わない。また演習 410 は、ほぼ elisp 版のプログラムの ことなので、これも省略する。残りの演習 43 から 46 までの 4 個について、さっきのプログラムに反映した。 ───────────────────────────────────────────────────── #!/usr/local/bin/emacs script (require 'cl) (require 'calc) (let (stack variable)
(while (not (string= "Q" (setq input (readstring "INPUT=> ")))) (cond ((stringmatch "^[09.]+$" input)
(push (stringtonumber input) stack)) ((string= "p" input)
(princ (format "stack: %S\n" stack))
(princ (format "variable: %S\n" variable))) ((string= "N" input) (if (< (length stack) 1) (princ "error: too few elements on stack\n") (push (* 1 (pop stack)) stack))) ((string= "X" input) (if (< (length stack) 2) (princ "error: too few elements on stack\n") (let ((top (pop stack)) (2nd (pop stack))) (push top stack) (push 2nd stack)))) ((string= "P" input) (if (< (length stack) 2) (princ "error: too few elements on stack\n") (let ((top (pop stack))) (push (mathpow (pop stack) top) stack)))) ((string= "E" input)
(if (< (length stack) 1) (princ "error: too few elements on stack\n") (push (exp (pop stack)) stack))) ((string= "S" input) (if (< (length stack) 1) (princ "error: too few elements on stack\n") (push (sin (pop stack)) stack))) ((string= "s" input) (if (< (length stack) 1) (princ "error: too few elements on stack\n")
(loop for n = (readstring "VARIABLE NAME=> ") for v = (assoc n variable) until (not v) finally (push (cons n (pop stack)) variable)))) ((string= "r" input) (if (< (length variable) 1) (princ "error: too few elements on variable\n") (loop for n = (readstring "VARIABLE NAME=> ") for v = (assoc n variable) until v finally (progn (push (cdr v) stack) (setq variable (delete v variable)))))) ((stringmatch "^[09AZaz]$" input)
(princ (format "error: unknown command %s\n" input))) ((stringmatch "^[+\\*/%]$" input) (if (< (length stack) 2) (princ "error: too few elements on stack\n") (if (and (string= "/" input) (or (equal 0 (car stack)) (equal 0.0 (car stack)))) (princ "error: zero divisor\n") (let ((p (pop stack))) (push (funcall (internsoft input) (pop stack) p) stack))))) ((string= "" input) (let ((p (pop stack))) (if p (princ (format "%f\n" p)) (princ "error: empty stack\n")))) (t
(princ (format "error: unknown command %s\n" input)))))) $ ./pcalc.el INPUT=> 1 INPUT=> 2 INPUT=> INPUT=> 4 INPUT=> 5 INPUT=> + INPUT=> * INPUT=> p stack: (9) variable: nil INPUT=> % error: too few elements on stack INPUT=> 3 INPUT=> % INPUT=> p stack: (0) variable: nil INPUT=> 10 INPUT=> p stack: (10 0) variable: nil INPUT=> x error: unknown command x INPUT=> X INPUT=> p stack: (0 10) variable: nil INPUT=> / error: zero divisor INPUT=> p stack: (0 10)
variable: nil INPUT=> 0.000000 INPUT=> p stack: (10) variable: nil INPUT=> 2 INPUT=> / INPUT=> p stack: (5) variable: nil INPUT=> s VARIABLE NAME=> a INPUT=> p stack: nil variable: (("a" . 5)) INPUT=> r VARIABLE NAME=> a INPUT=> p stack: (5) variable: nil INPUT=> Q ───────────────────────────────────────────────────── こんな無茶苦茶なプログラムでも意外にちゃんと動く。ただし、C 言語で作るとなると、考えただけでゾッとする。四則 演算部分を共通化するために、入力を正規表現で判定しようとしているが、意図しないものに引っ掛かるようなので、 直前でせき止めるような姑息な手段を使っている。変数への退避は variable というスタックを別に設けて、それを使 用するようにした。変数名の入力は該当するものが見つかるまで続け、キャンセルは効かない。演習前のプログラムは エラー処理が省略されているので、こちらの方が少しは安全な処理になっている。 C 言語の外部変数に関しては、外部変数にしたからといって、それを安易に他のプログラマに直接参照させるのは良くな い。外部変数は常にどこからでも参照、変更が可能だが、それは同時に、いつ誰がその変数の内容を元に処理している か分からないことを意味する。場合によっては、処理中に他から参照されると困る場合もあり得る。そういうことが起 こり得る前提であれば、参照関数や設定関数を外部関数として用意し、変数は隠蔽する方が安全である。また、そう いった参照関数や設定関数も、複数の処理から常に同時に呼ばれても良いような構造(スレッドセーフな構造?)になっ ていなければならない。
4.4
4.4
通用範囲に関する規則
通用範囲に関する規則
本節では関数や変数のスコープの話をしていると思うのだが、当たり前のことを書籍に書こうとすると、なぜこんなに 分かりにくい話になるのか不思議だ。 ● コンパイル中に適切に変数を宣言するにはどのような宣言を書けば良いか? ● すべての部分をプログラムのロード時に正しく結合するには、宣言をどのように組み立てれば良いか? ● コピーが一つですむようにするには、宣言をどうすれば良いか? ● 外部変数をどう初期化するか? コンパイル中に変数を宣言するとは、どういう意味なのか。パッと見、cc コマンド実行中に変数を宣言する??と読めな くもない。これは実行形式を作成するためのリンケージを行なう前の段階で...と読み替えないといけない。複数人で開 発する場合、プログラムの作成も分担するため、それぞれが作成後に個々にソースファイルをコンパイルする。この段階で、外部変数の実体の定義と、外部宣言(extern 宣言:その変数は他のファイルで定義されていることを示す宣言) をどのように行なうかを考える必要がある。こういうことを考えるのは、たいてい PJ リーダの次あたりのポジションの 人であり、その人のセンスいかんで、作業がスムーズにいくかどうかが決まってしまう。 正しく結合するには、外部関数や外部変数の実体定義が結合する範囲でユニークに存在する状態にすれば良いだけだ。 その方法はいろいろある。外部関数はどう分担しても分散することが避けられないが、外部変数定義は extern.c など の名前のファイルにまとめたり、外部変数宣言をまとめて extern.h というヘッダにまとめたりする場合がある。これ は、個々のソースファイルを手っ取り早くコンパイルするのに効率的な方法かもしれないが、外部関数や外部変数のス コープというものをまったく考えないやり方だ。そもそもコンパイルする段階になって、extern.c や extern.h を間 に合わせで作成しているようでは、既に手遅れであり、その段階でスコープをあれこれ云々しても無意味である。ス コープを気にするのであれば、外部変数は必ずどれかの機能を実現するために存在するものなので、その機能に閉じて 定義すべきである。これは外部関数についても同様である。 外部変数の初期化は、その外部変数を持つ機能がアクティブな状態になる前に行なわれるべきである。さらに、機能に よっては、他の機能を使用して初期化する場合があるので、初期化順序に関してはよく検討する必要がある。 このような話は、C++や Java などの言語では改善されているはずなので、C 言語でプログラムを書かない限り、気にす る必要がないのかもしれない。また、それらの言語は C 言語のように外部変数の実体をどこか一ヶ所に定義するという 概念がそもそも無いと思われる。 では、elisp ではどうか。 elisp においては、ファンクションも変数もすべてグローバルである。既に書いたかもしれないが、極論すれば、let を使って局所的に...と思っても、それがスタック上に見つかれば、それが意図する変数かどうかに関わらず参照出来て しまう。 (defun localmania () local1)^J localmania (block nil (let ((local1 "Viva Local!")) (let (local2) (let (local3) (return (localmania))))))^J "Viva Local!" これは、C 言語でもローカル変数のポインタを持ち回れば、どこでもその変数を参照したり変更したり出来るのと似てい る。ただ、それが出来るならそうすれば良いとか、ローカルバインディングなんて意味が無いとは思わない。elisp は ダイナミックスコープを採用しているようだが、Emacs 24.1 からレキシカルスコープもオプションでサポートされた ようだ。今使っている Emacs は 25.0.50 なので、新しいバッファを開いて確かめてみる。 (setq lexicalbinding t)^J t (defun localmania () local1)^J localmania (symbolfunction 'localmania)^J (closure (t) nil local1)
(block nil (let ((local1 "Viva Local!")) (let (local2) (let (local3) (return (localmania))))))^J Debugger enteredLisp error: (voidvariable local1) まあ、確かにエラーになるようだが、だから何?というか、イタズラするだけでは意味が無いし、スコープの特性を意識 してプログラムを書いているわけでは無いので、とりあえずレキシカルスコープは一旦忘れることにする。
(defun sampleobject (command &optional args) (or (boundp 'localstack)
(set (intern "localstack") nil))
(cond ((eq 'put command) (and args (push args localstack))) ((eq 'get command) (pop localstack)) ((eq 'print command) localstack)))^J sampleobject (sampleobject 'put '(1 2 3))^J ((1 2 3)) (sampleobject 'print)^J ((1 2 3)) (setf (car (sampleobject 'print)) '(3 2 1))^J (3 2 1) (sampleobject 'print)^J ((3 2 1)) これは、localstack という名前のスタック変数を表に出さないように、sampleobject という関数の中に隠蔽した つもりが、実は外部から変更可能だったという間抜けな例である。sampleobject 関数の print 機能が返すのは、隠 蔽しているつもりの localstack だが、それをそのまま返却すると、返ってきたオブジェクト(コンスセル)の car は、localstack の car と同じものであり、これを setf で指定すると localstack の中身を変更できてしまうのだ。
(defun cpseqrecur (obj) (if (sequencep obj) (let (result) (dolist (elm obj) (setq result (cons (cpseqrecur elm) result))) (reverse result)) (purecopy obj)))^J cpseqrecur (defun sampleobject (command &optional args) (or (boundp 'localstack)
(set (intern "localstack") nil)) (cond ((eq 'put command) (and args (cpseqrecur (push args localstack)))) ((eq 'get command) (cpseqrecur (pop localstack))) ((eq 'print command) (cpseqrecur localstack))))^J sampleobject (sampleobject 'put '(1 2 3))^J ((1 2 3)) (sampleobject 'print)^J
((1 2 3)) (setf (car (sampleobject 'print)) '(3 2 1))^J (3 2 1) (sampleobject 'print)^J ((1 2 3)) これは、間抜けな sampleobject 関数を少しだけ改善したものである。返却するオブジェクトを localstack のコ ピーにしている。これにより返ってきたオブジェクトを setf で変更しても、localstack の中身は変わらない。 localstack のコピーは copysequence でも良いような気がするが、コンスセルを再帰的にコピーしてやらないと、 リストの中にさらにリストがある場合に外部から変更可能になってしまうようだ(実験済み)。 通用範囲の規則ということで、不自然なプログラム例を載せたが、あまり神経質になる必要はない。スコープよりは、 LISP オブジェクトの振る舞いの方に注意した方が良いと思われる。
4.5
4.5
ヘッダ・ファイル
ヘッダ・ファイル
この節の内容には、これまで書いてきた内容に照らして、技術的に注目すべき点がほとんど見当たらない。少なくとも、 C プログラムをどのようにファイルに配置するかという観点で、機能の局所化や機能の境界に関する議論が何も無いよう なので、本節はスキップする。そもそも、elisp にヘッダ・ファイルは存在しない。4.6
4.6
静的変数
静的変数
変数定義に static を付けると、静的な変数になる?というのも意味不明だ。もともと C 言語に動的な変数はないので、 静的な変数という言い方は誤解を招く。ファイル内でのみ参照、変更可能な変数という意味の適当な名称を考えるべき である。さらに、ファイル内の定義場所によっても動作が変わる。関数の外に置いた場合は、ファイル内のどの関数か らも参照、変更可能だが、関数の中に置いた場合は、その関数以外からは見えない変数になり、また関数からリターン した後も値を保持する。整理すると、ファイル内の置いた場所に応じたスコープを持ち、変数の値は同様に保持される。 先ほど書いた、elisp の sampleobject 関数の中に隠蔽した変数は、C 言語でいう関数内スタティック変数と同じ性 質を持つ。それ以外、この節も前節と同様に特筆すべき事項はない。 演習 411 は、なぜか電卓プログラムの ungetch にこだわっている。割愛する。4.7
4.7
レジスタ変数
レジスタ変数
読んで字のごとく、CPU のレジスタを変数にしてしまおう、ということなのかもしれないが、コンパイラがレジスタを どのように使用するかを知らないで、あるいは、あるレジスタをあるオート変数専用にしようとした場合に、コンパイ ラがそれをどの程度考慮するのか、そういったコンパイラの仕様をロクに知らないで、何となく処理速度が向上しそう だからと、余計なことをしない方が良いというのが、私個人の考え方である。演算の過程で、あるオート変数の値をレ ジスタにロードすることは頻繁にあるわけで、そういうのを無視してあるレジスタをある変数専用にするようコンパイ ラに指示するというのが、感覚的にあり得ないのだ。ただ、むかしから C コンパイラはデフォルトの最適化でも、ある 変数をレジスタに割り当てることがあるらしいので、それならばわざわざ register というキーワードを書かなくての 良いのではないかと思われる。 そもそもコンパイラが吐くコードは、C 言語のプログラムをアセンブラコードに翻訳したものであって、普通に、最初からアセンブラで書いたら、誰もそんな書き方はしないという、冗談のようななコードが多く見受けられる。それは、コ ンパイラが人の心や目的を理解する愛人や心理学者ではないからである。 elisp においては、シンボルにしろ、LISP オブジェクトにしろ、レジスタに割付可能なシロモノではないので、そう いった概念はまったくない。
4.8
4.8
ブロック構造
ブロック構造
タイトルのイメージに反して、ここではブロック内変数のことが紹介されている。私はもともと出来るだけローカル変 数を使わないようにプログラムを書きたいので、大括弧でプロックを作ると、その中でさらに新たなローカルな変数が 使えますよ、的な呑気な物言いでこれを紹介することに賛成ではない。こういう、プログラミング言語の機能追加は、 それがもたらすメリットよりも、リスクの方が大きいからである。もしも、関数の途中でいくつものネストしたブロッ クが登場し、その中で外部変数、オート変数、スタティック変数が乱れるように使用されていたら、書いた本人は良く ても、周囲には理解出来ないようなシロモノになる可能性がある。そういったブロックが無く、オート変数しか使用し ていなくても、C 言語のソースを追うのはたいへんなのだ。 もちろん、C 言語のことを知り尽くしたエキスパートなら、そういう機能にメリットを感じても、デメリットを感じるこ とはないのかもしれない。しかし、C 言語でプログラムを書くのはエキスパートばかりではない。総じて、プログラミン グ言語の機能拡張は、エキスパートをメインターゲットにやっているものばかりで、つい先日入社したばかりの新人の ことなどまったく考えてはいない。しかし、エキスパートだろうが、新人だろうが、ど素人だろうが同じコンパイラを 使うのである。 また、どこの開発組織でも、プログラミング言語の仕様変更の都度、コーディング規約を見直すのは大変であり、変更 によって、開発規模が変わったり、生産性が変わったり、品質指標の見直しが発生したりすることに耐えられないため、 そういうことには積極的では無いはずである。当然ながら、elisp にも同様の問題がある。たとえば、let フォームはどこにでも書けるので、let のバインディング に let が現れたり、if 文の then 部分に let が現れたりなどというのは日常茶飯事である。そのため、そういう場所で let を使用する場合は、そこに閉じた変数に徹するよう注意しなければならない。
4.9
4.9
初期化
初期化
本節では、プログラムで扱うデータの初期化について述べられている。初期化するかどうか、初期化されているかどう かを気にするのは、我々が書くプログラムに制御が渡る前から、既にデータが存在しているからである。外部変数や静 的変数は当然として、関数の中では処理を書く前に変数を宣言する。実際の関数の動きも、スタック内にローカル変数 の領域が確保されてから、関数内の最初の処理に制御が渡る。よって、宣言した変数を初期化しなければ、その内容は 不定となるのは当然であって、なんら不思議ではない。 往々にしてプログラマは最初のテストで、たまたまある変数の初期値が 0 だったせいで OK と判断してしまい、別の人の テストでバグを出してしまうことがよくある。しかし、必ず何らかの関数の戻り値を設定する処理結果の変数を宣言時 に初期化するというのは、明らかに神経質になり過ぎだ。また、そのようなことがパッと見て分からないほど、大きく て複雑な関数を書くべきではない。 原著には、いくつかのタイプの変数の初期化方法について記載されている。char pattern[] = “ould”; ANSI 規格になってから、ポインタ変数なのに配列の先頭アドレスであるかのように、記述することが多くなっているが、 これは pattern という変数を char *pattern のように使用しないようにする意図があると思われる。私が C 言語で開 発を行なっていた頃は、上の pattern 変数においては、char 型の配列に文字コードを文字列になるよう設定している のではなく、コンスタントデータのセクションに"ould”というデータを置き、その先頭アドレスを pattern に設定し ているだけ、と教わった。しかし、上記の宣言だけを書いたプログラムを gdb で調べてみると、たしかにスタック上 に"ould”という文字列が見つかる。データの実体がどこにあるのかは、エリア破壊が起きてプログラムが正しく動作し ない場合の解析において、非常に重要なことになる。
4.10
4.10
再帰
再帰
C の関数は再帰的に使用できる。つまり、関数は自分自身を直接または間接に呼び出してよい。いま、数字を文字列とし て印字する関数を考えよう。すでに述べたように数字は逆順生成される。すなわち、低い桁の数字が高い桁の数字より先に得られるが、印字するときは反対にしなければならない。 (defun printd (n) (and (< n 0) (progn (princ (format "%c" ?)) (* n 1))) (or (zerop (/ n 10)) (printd (/ n 10))) (princ (format "%c" (+ (% n 10) ?0))))^J printd (tracefunction 'printd)^J nil (let ((s "1234")) (printd (stringtonumber s)) (princ "\n"))^J 1234 " " ====================================================================== 1 > (printd 1234) | 2 > (printd 123) | | 3 > (printd 12) | | | 4 > (printd 1) | | | 4 < printd: "1" | | 3 < printd: "2" | 2 < printd: "3" 1 < printd: "4" この例における、整数を文字列に変換するロジックは、すでに書いているように、変換対象の数値を基数(ここでは 10)で割った余りを求め、その余りの文字コードを記憶し、その後、基数で割って基数の分だけ右にシフトする(処理 が済んだ一番下の桁が消える)、というものである。このロジックだと、一番下の桁から順に上の桁に向かって処理し ていくため、得られた文字コードの記憶順が、表示したい順と逆になる。この逆順を反転させなくて済むようにするの は、printd という関数を再帰的に呼び出すとよい、というのが上の例の説明である。 再帰呼び出しに慣れていない人は、上記のように、tracefunction を使って printd 関数のトレースを表示するよう 設定しておくと分かり易い。このトレースでは、printd 関数呼び出したときの引数と、printd 関数から復帰するとき の返り値を表示している。この処理では変換対象 n を 10 で割って 0 になるまで、再帰呼び出しを繰り返し、0 になると その下の princ を実行し始める。つまり、文字が一番上の桁までいってから、下の桁に向かって順に印字されるように ロジック化しているわけだ。 このような再帰の動作は、各呼び出し毎にすべてのローカル変数が一新され、以前のセットとまったく独立になる。と 原著には書かれているが、実際にはローカル変数だけでなく、呼び出し時にリターン先のインストラクションアドレス や、呼び出し前のベースポインタなども一緒にスタックに積まれる。なので、どんどん再帰呼び出しをしていけば、い ずれスタックを使い果たしてしまう。elisp の場合は、maxlispevaldepth という変数(使用中 Emacs の定義値 は 800)で関数コールの最大深度を規定していて、これを超える呼び出しをした場合はエラーとなる。
(defun recursivecall (a) (recursivecall (1+ a)))^J recursivecall
(tracefunction 'recursivecall)^J nil
(let ((a 0)) (recursivecall a))^J Debugger enteredLisp error: (error "Lisp nesting exceeds ‘maxlispevaldepth’") ====================================================================== 1 > (recursivecall 0) | 2 > (recursivecall 1) | | 3 > (recursivecall 2) | | | 4 > (recursivecall 3) | | | | 5 > (recursivecall 4) | | | | | 6 > (recursivecall 5) | | | | | | 7 > (recursivecall 6) | | | | | | | 8 > (recursivecall 7) | | | | | | | | 9 > (recursivecall 8) | | | | | | | | | 10 > (recursivecall 9) ... ... ... | | | | | | | | | | | | | | | | | | | | | | | | | | 158 > (recursivecall 157) このような単純な無限再帰呼び出しをすると、トレースバッファの表示では 158 回呼んだところでエラーになる。これ と定義値 800 との関連は不明であるが、もしかしたらトレースしているからかもしれない。逆に言えば、トレースで確 認できる深度はこの程度ということになる。再帰呼び出しされる関数内であれこれ処理をしていたら、さらに少ない回 数で制限値に達する可能性がある。もちろん、maxlispevaldepth の定義値を大きくすれば、さらに深く再帰する ことは可能かもしれないが、そもそもこの定義値は Emacs のスタックサイズを元に定義されているので、値を大きくす るのはリスクを伴う。最悪の場合、Emacs 自体がスタックオーバーフローで異常終了するかもしれない。 私の個人的としては、処理対象のデータが再帰的な構造を持つ(たとえば、階層ディレクトリを順番に辿りながらすべ てのファイルを対象に処理するような)場合でない限り、再帰呼び出しはしない方が良いと考えている。世の中には 「ループとは再帰呼び出しである」と考えている人もいるようで、どんなロジックでも、まずは再帰にならないかとど うかを検討するらしいが、何年プログラミングをやっていてもそういう考え方には馴染まない。再帰を好む傾向は、 LISP の派生言語 Scheme(再帰呼び出しをループに変換して処理できるため、スタックオーバーフローのリスクがな い?)が登場して、さらに拍車がかかった?ようである。 ───────────────────────────────────────────────────── #include <stdio.h> #include <stdlib.h> #include <time.h> #define VMAX 10
void swap(int v[], int i, int j) { int temp; temp = v[i]; v[i] = v[j]; v[j] = temp; }
void qqsort(int v[], int left, int right) {
int i, last;
return; swap(v, left, (left + right) / 2); last = left; for (i = left + 1; i <= right; i++) if (v[i] < v[left]) swap(v, ++last, i); swap(v, left, last); qqsort(v, left, last 1); qqsort(v, last + 1, right); } int main() { int i, *v; unsigned seed; time_t timev;
seed = (unsigned)time(&timev); srand(seed);
v = (int *)malloc(sizeof(int) * VMAX); for (i = 0; i < VMAX; i++) { v[i] = rand(); } for (i = 0; i < VMAX; i++) { printf("%d\n", v[i]); } qqsort(v, 0, VMAX 1); printf("\n"); for (i = 0; i < VMAX; i++) { printf("%d\n", v[i]); } return 0; } $ ./qsort 1734668863 1247168748 648008390 1452012412 1742119902 1133324182 1640324974 1024098847 1635102910 677554906 648008390 677554906 1024098847 1133324182 1247168748 1452012412 1635102910 1640324974 1734668863 1742119902 ───────────────────────────────────────────────────── (require 'cl)^J
(defun qsort (left right) (block ()
(let (i last (swap (lambda (i j) (let ((temp (aref v i))) (aset v i (aref v j)) (aset v j temp))))) (and (>= left right) (return)) (funcall swap left (/ (+ left right) 2)) (setq last left) (loop for i from (1+ left) until (> i right) do (and (< (aref v i) (aref v left)) (funcall swap (setq last (1+ last)) i))) (funcall swap left last) (qsort left (1 last)) (qsort (1+ last) right))))^J qsort (defun main () (let ((seed (mapconcat 'numbertostring (reverse (currenttime)) "")) (v (makevector 10 0))) (random seed) (loop for i below 10 do (aset v i (abs (random)))) (loop for i below 10 do (princ (format "%d\n" (aref v i)))) (qsort 0 9) (princ "\n") (loop for i below 10 do (princ (format "%d\n" (aref v i))))))^J main (main)^J 238798587 136878771 231021585 494619835 418038770 350950917 522669215 356247796 269773817 262387612 136878771 231021585 238798587 262387612 269773817 350950917 356247796 418038770 494619835 522669215 nil ───────────────────────────────────────────────────── qsort のプログラム例であるが、C 言語の方は名前を qqsort に変更しただけ、elisp の方はそれを書き直しただけで ある。swap は let でバインドし、funcall で呼び出すようにしている。また、ソート対象のベクトルは qsort の引数 にせずに、main 関数の中の変数を参照するようにした。C 言語のようにポインタで持ち回ることが出来ないからである。 あと、ループについては、簡潔に書けるという理由で、意識的に loop マクロを使うようにしている。
まあ、あまり目新しいことはないが、こういう書にはプログラム例がいろいろ載っている方が、こちらの意図とは別の ところで役立つこともあると思われる。実は、本節のプログラムは Scheme でも書いてみようと、いろいろ調べたのだ が、ネット上の情報だけですぐにホイホイ書けるほど簡単ではなかったのだ(プログラム例が少な過ぎて、書いてある
ことがよく分からない)。ほとんどいないとは思うが、この書で elisp をマスターしたいと思う人には、やはりプログ ラム例は多い方が助かると思う。
演習
演習
412
412
printd のアイデアを使って itoa の再帰版を書け。すなわち、再帰ルーチンを呼ぶことによって整数を文字列に変換せ よ。 (defun itoa (n) (and (< n 0) (progn (setq s (concat s "")) (* n 1))) (or (zerop (/ n 10)) (itoa (/ n 10))) (setq s (concat s (format "%c" (+ (% n 10) ?0)))))^J itoa (let ((n (stringtonumber (readstring "Input=> "))) (s "")) (itoa n))^J "1234" ここでも、呼び出し元のローカルバインド(s)を参照して処理している。それ以外は、printd 関数の putchar して いるところを変数 s への文字連結に変更しているだけ。演習
演習
413
413
文字列 s をその場所で逆順にする関数 reverse(s)の再帰版を書け。(defun reverse (string)
(let ((vector (stringtovector string)) (swap (lambda (v i j) (let ((temp (aref v i))) (aset v i (aref v j)) (aset v j temp)) v))) (flet ((reverse1 (vector left right) (and (< (1+ left) (1 right)) (reverse1 vector (1+ left) (1 right))) (funcall swap vector left right))) (reverse1 vector 0 (1 (length vector)))) (mapconcat 'chartostring vector "")))^J (reverse "abcdefg")^J "gfedcba"
問題が reverse 関数を書けなのでこうしたが、elisp には reverse という関数があるので名前は変えた方が良い。問 題が期待する解答は配列をアクセスするプログラムだと思われるので、reverse では処理前に文字列→配列、処理後に 配列→文字列の変換を行ない、逆順は flet で内部に定義した reverse1 が行なっている。このプログラムは、私の再 帰に対するモチベーションの低さをよく表していると思う。 (let ((testv (stringtovector "abcdefg")) (swap (lambda (v i j) (let ((temp (aref v i))) (aset v i (aref v j))
(aset v j temp)) v))) (setq testv (funcall swap testv 0 6)) (setq testv (funcall swap testv 1 5)) (setq testv (funcall swap testv 2 4)) (mapconcat 'chartostring testv ""))^J "gfedcba" 要するに、上記のように処理できれば良いので、それを再帰に書き換えただけなのだ。
4.11 C
4.11 C
のプリプロセッサ
のプリプロセッサ
elisp にプリプロセッサはたぶん無い。なので、ここは原著のタイトルのままとしている。ある種の言語仕様が与えら れると書かれているが、どちらかと言えば実装仕様ではなかろうか。 $ make qsort CFLAGS=v\ g cc v g qsort.c o qsort Using builtin specs. COLLECT_GCC=cc COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686linuxgnu/4.9/ltowrapper Target: i686linuxgnu Configured with: ../src/configure v withpkgversion='Ubuntu 4.9.210ubuntu13' with bugurl=file:///usr/share/doc/gcc4.9/README.Bugs enablelanguages=c,c+ +,java,go,d,fortran,objc,objc++ prefix=/usr programsuffix=4.9 enableshared enablelinkerbuildid libexecdir=/usr/lib withoutincludedgettext enable threads=posix withgxxincludedir=/usr/include/c++/4.9 libdir=/usr/lib enablenls withsysroot=/ enableclocale=gnu enablelibstdcxxdebug enablelibstdcxx time=yes enablegnuuniqueobject disablevtableverify enableplugin with systemzlib disablebrowserplugin enablejavaawt=gtk enablegtkcairo with javahome=/usr/lib/jvm/java1.5.0gcj4.9i386/jre enablejavahome withjvmroot dir=/usr/lib/jvm/java1.5.0gcj4.9i386 withjvmjardir=/usr/lib/jvmexports/java 1.5.0gcj4.9i386 witharchdirectory=i386 withecjjar=/usr/share/java/eclipse ecj.jar enableobjcgc enabletargets=all enablemultiarch disablewerror with arch32=i686 withmultiliblist=m32,m64,mx32 enablemultilib withtune=generic enablechecking=release build=i686linuxgnu host=i686linuxgnu target=i686 linuxgnu Thread model: posix gcc version 4.9.2 (Ubuntu 4.9.210ubuntu13) COLLECT_GCC_OPTIONS='v' 'g' 'o' 'qsort' 'mtune=generic' 'march=i686' /usr/lib/gcc/i686linuxgnu/4.9/cc1 quiet v imultiarch i386linuxgnu qsort.c quiet dumpbase qsort.c mtune=generic march=i686 auxbase qsort g version fstack protectorstrong Wformat Wformatsecurity o /tmp/ccyTF45s.s GNU C (Ubuntu 4.9.210ubuntu13) version 4.9.2 (i686linuxgnu) compiled by GNU C version 4.9.2, GMP version 6.0.0, MPFR version 3.1.2p11, MPC version 1.0.3 GGC heuristics: param ggcminexpand=100 param ggcminheapsize=131072 ignoring nonexistent directory "/usr/local/include/i386linuxgnu" ignoring nonexistent directory "/usr/lib/gcc/i686linuxgnu/4.9/../../../../i686linux gnu/include" #include "..." search starts here: #include <...> search starts here: /usr/lib/gcc/i686linuxgnu/4.9/include /usr/local/include /usr/lib/gcc/i686linuxgnu/4.9/includefixed /usr/include/i386linuxgnu /usr/include End of search list. GNU C (Ubuntu 4.9.210ubuntu13) version 4.9.2 (i686linuxgnu) compiled by GNU C version 4.9.2, GMP version 6.0.0, MPFR version 3.1.2p11, MPC version 1.0.3 GGC heuristics: param ggcminexpand=100 param ggcminheapsize=131072Compiler executable checksum: 8802821ddd4be89d816de7ae5cb1d6d0
COLLECT_GCC_OPTIONS='v' 'g' 'o' 'qsort' 'mtune=generic' 'march=i686' as v 32 o /tmp/ccTzNH32.o /tmp/ccyTF45s.s
GNU アセンブラ バージョン 2.25 (i686linuxgnu)、BFD バージョン (GNU Binutils for Ubuntu) 2.25 を使用 COMPILER_PATH=/usr/lib/gcc/i686linuxgnu/4.9/:/usr/lib/gcc/i686linux gnu/4.9/:/usr/lib/gcc/i686linuxgnu/:/usr/lib/gcc/i686linuxgnu/4.9/:/usr/lib/gcc/i686 linuxgnu/ LIBRARY_PATH=/usr/lib/gcc/i686linuxgnu/4.9/:/usr/lib/gcc/i686linux gnu/4.9/../../../i386linuxgnu/:/usr/lib/gcc/i686linux gnu/4.9/../../../../lib/:/lib/i386linuxgnu/:/lib/../lib/:/usr/lib/i386linux gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686linuxgnu/4.9/../../../:/lib/:/usr/lib/ COLLECT_GCC_OPTIONS='v' 'g' 'o' 'qsort' 'mtune=generic' 'march=i686' /usr/lib/gcc/i686linuxgnu/4.9/collect2 plugin /usr/lib/gcc/i686linux gnu/4.9/liblto_plugin.so pluginopt=/usr/lib/gcc/i686linuxgnu/4.9/ltowrapper plugin opt=fresolution=/tmp/ccMRVh7D.res pluginopt=passthrough=lgcc pluginopt=pass through=lgcc_s pluginopt=passthrough=lc pluginopt=passthrough=lgcc plugin opt=passthrough=lgcc_s sysroot=/ buildid ehframehdr m elf_i386 hash style=gnu asneeded dynamiclinker /lib/ldlinux.so.2 z relro o qsort /usr/lib/gcc/i686linuxgnu/4.9/../../../i386linuxgnu/crt1.o /usr/lib/gcc/i686linux gnu/4.9/../../../i386linuxgnu/crti.o /usr/lib/gcc/i686linuxgnu/4.9/crtbegin.o L/usr/lib/gcc/i686linuxgnu/4.9 L/usr/lib/gcc/i686linuxgnu/4.9/../../../i386linux gnu L/usr/lib/gcc/i686linuxgnu/4.9/../../../../lib L/lib/i386linuxgnu L/lib/../lib L/usr/lib/i386linuxgnu L/usr/lib/../lib L/usr/lib/gcc/i686linuxgnu/4.9/../../.. /tmp/ccTzNH32.o lgcc asneeded lgcc_s noasneeded lc lgcc asneeded lgcc_s noasneeded /usr/lib/gcc/i686linuxgnu/4.9/crtend.o /usr/lib/gcc/i686linux gnu/4.9/../../../i386linuxgnu/crtn.o v オプションを付けてコンパイルすると、こんなにたくさんの情報が表示される。バージョン 2 の頃の gcc では、はっ きりと cpp のコマンドラインが出ていたが、上の情報の中にはそういう明確なものはなさそうだ。しかし、ちょっとし た例題プログラムのコンパイルをするにも、こんなややこしいコマンドをいろいろと実行していることは分かる。
4.11.1
4.11.1
ファイルの取り込み
ファイルの取り込み
elisp にプリプロセスは無いが、ファイルの取り込みという意味ではローディングがある。逆に C 言語にはローディン グは無い(シェアードライブラリは実行時にローディングされるが、これは C プログラムから制御しているわけではな い)。elisp でローディングするライブラリは、Emacs のインストールディレクトリ配下にある lisp ディレクトリにある (/usr/share/emacs/バージョン/lisp または/usr/local/share/emacs/バージョン/lisp など)。Emacs を ソースからビルドすると Help からライブラリのソース(.c、.el ファイル)を参照できるが、パッケージでインス トールするとソースを見ることは出来ない。これらの lisp ディレクトリは、デフォルトで loadpath 変数に設定され ており、フルパスを指定しなくてもローディングできる。 loadpath^J ("/home/tanaka/elisp" "/usr/local/share/emacs/25.0.50/sitelisp" "/usr/local/share/emacs/sitelisp" "/usr/local/share/emacs/25.0.50/lisp" "/usr/local/share/emacs/25.0.50/lisp/vc" "/usr/local/share/emacs/25.0.50/lisp/url" "/usr/local/share/emacs/25.0.50/lisp/textmodes" "/usr/local/share/emacs/25.0.50/lisp/progmodes" "/usr/local/share/emacs/25.0.50/lisp/play" "/usr/local/share/emacs/25.0.50/lisp/org" "/usr/local/share/emacs/25.0.50/lisp/nxml" "/usr/local/share/emacs/25.0.50/lisp/net" "/usr/local/share/emacs/25.0.50/lisp/mhe" "/usr/local/share/emacs/25.0.50/lisp/mail" "/usr/local/share/emacs/25.0.50/lisp/leim" "/usr/local/share/emacs/25.0.50/lisp/language" "/usr/local/share/emacs/25.0.50/lisp/international" "/usr/local/share/emacs/25.0.50/lisp/gnus" "/usr/local/share/emacs/25.0.50/lisp/eshell"
"/usr/local/share/emacs/25.0.50/lisp/erc" "/usr/local/share/emacs/25.0.50/lisp/emulation" "/usr/local/share/emacs/25.0.50/lisp/emacslisp" "/usr/local/share/emacs/25.0.50/lisp/cedet" "/usr/local/share/emacs/25.0.50/lisp/calendar" "/usr/local/share/emacs/25.0.50/lisp/calc" "/usr/local/share/emacs/25.0.50/lisp/obsolete") これらのディレクトリにあるファイルをロードするには、load、loadlibrary、require を使用するが、基本的な ファンクションを使用するのにいちいち関連するライブラリをロードする必要は無い。これは Emacs をビルドする際に デフォルトでローディングされているからである。また、それ以外の多くのファンクションも autoload という設定が されており、使用すると自動的にローディングされるようになっている。 (load "cl")^J t (loadlibrary "cl")^J t (require 'cl)^J cl (featurep 'cl)^J t (featurep 'stdio) nil loop マクロを使用する際に必要となる、Common Lisp 互換のライブラリは上記のようにロードすることが可能である。 この中で 3 番目の require によるローディングは、(provide 'cl)と宣言されている場合に有効となる。また、ロー ド済みの場合はそれを繰り返さない。require 可能なライブラリは、featurep により既にロード済みかどうかを確認 できる。 目的とするライブラリがどのファイルで定義されているかは、Help(Ch f ファンクション名)を参照する。 loop is an alias for ‘clloop’. ↓ clloop is an autoloaded Lisp macro in ‘clmacs.el’. (loop CLAUSE...)
The Common Lisp ‘loop’ macro. Valid clauses include: For clauses: for VAR from/upfrom/downfrom EXPR1 to/upto/downto/above/below EXPR2 by EXPR3 for VAR = EXPR1 then EXPR2 for VAR in/on/inref LIST by FUNC for VAR across/acrossref ARRAY for VAR being: the elements of/ofref SEQUENCE [using (index VAR2)] the symbols [of OBARRAY] the hashkeys/hashvalues of HASHTABLE [using (hashvalues/hashkeys V2)] the keycodes/keybindings/keyseqs of KEYMAP [using (keybindings VAR2)] the overlays/intervals [of BUFFER] [from POS1] [to POS2] the frames/buffers the windows [of FRAME] Iteration clauses: repeat INTEGER while/until/always/never/thereis CONDITION Accumulation clauses: collect/append/nconc/concat/vconcat/count/sum/maximize/minimize FORM
[into VAR] Miscellaneous clauses: with VAR = INIT if/when/unless COND CLAUSE [and CLAUSE]... else CLAUSE [and CLAUSE...] named NAME initially/finally [do] EXPRS... do EXPRS... [finally] return EXPR For more details, see Info node ‘(cl)Loop Facility’.
loop のヘルプは上のように表示される。ファイル名は clmacs.el と表示されているが、これは(require 'cl)によ りロードされる。残念ながら、ヘルプが日本語化されたことはないが、プログラマなら必要最低限のことは分かるよう になっている。また、むかしの Emacs と違って、elisp ソースや Info へのリンクが付くようになり、バージョンアッ プの度にどんどん改善されている。
clloop is an autoloaded Lisp macro と書かれているので、実行時に cl や clmacs はロードされるが、バッ チモード(script オプション)では require が必要となるようだ。また、ロードされる前は fontlock(キー ワードの色付け)されなかったり、定義されているようにインデントされなかったりするので、実行前にロードするこ とが多い。
4.11.2
4.11.2
マクロの置換
マクロの置換
#define name 置換テキスト C 言語でもっともシンプルなマクロ置換はこのようなものである。C プログラム中に name が現れた場合、コンパイル前 に置換テキストに置き換える。これは、第 1 章の記号定数の節で既に触れているとおりである。プログラムの中にマ ジックナンバーを埋め込まない、あるいは定数のメンテナンスを行ない易くするための方法として、#define を使用す るというのが C プログラマの常識になっている。しかし、仮にどのようにうまく定数の定義を行なったとしても、これ が固定値であることに変わりは無い。 (defconst name 値)elisp で固定値に名前を付けて使用する場合、たいてい defconst か defvar を使用する。これはマクロではなく変数 なので、値を変更できる。defvar の場合は、既にその変数が存在する場合、指定された値による初期化は行なわない。 これは同名の変数をユーザ初期化ファイル(~/emacs.d/init.el または~/.emacs)で定義していると、ライブラリ の定義を変更できるという振る舞いをする。それ以上の詳細については、あまり意識したことが無い。 (defmacro ++ (n) `(setq ,n (1+ ,n)))^J ++ (symbolfunction '++)^J (macro lambda (n) (list (quote setq) n (list (quote 1+) n))) (let ((a 0)) (list a (1+ a) (++ a) a))^J (0 1 1 1) (let ((a 0)) (macroexpand '(++ a)))^J (setq a (1+ a)) elisp のマクロは C 言語のそれよりはもう少し高度なものである。C 言語の場合は、仮にマクロが使えなくてもプログ