...

第 1 章 やさしい入門

by user

on
Category: Documents
9

views

Report

Comments

Transcript

第 1 章 やさしい入門
第 1 章 やさしい入門
まずは、Emacs-Lisp を手短に紹介することから始める。われわれの目的は、細かい点や、公
式の規則や、例外に深入りせず、現実のプログラムにかかわる Emacs-Lisp の本質を示すことで
ある。ここでは、完全あるいは正確であることは二の次とする(あげた例は正しいつもりだ
が)。読者には、できるだけ早く役に立つプログラムが書けるようになってもらいたい。それ
には、変数と定数、算術、制御の流れ、関数、基本的な入出力といった基本事項に注意を集中
する必要がある。より大きなプログラムを書くために非常に重要な Emacs-Lisp の特徴は、ここ
では省くことにする。そういったものは、シンボル、構造体、豊富にある関数の大部分、いく
つかの制御の流れの文、それに数多くのこまごました機能などである。
もちろん、このやり方には欠点もある。最も大きなものは、特定言語の特徴について、まと
まった話が一箇所にまとまらないことと、説明が舌足らずで誤解を招きやすいことである。そ
れから、例だけでは Emacs-Lisp の全機能を使えないから、例は意図したほど簡潔にもエレガン
トにもなりえない恐れもある。われわれとしてはそうならないように努めたが、注意はしてい
ただきたい。もう一つの欠点は、後の章で本章の一部を繰り返す必要性があるということであ
る。ただし、この繰返しは、読者にとってわずらわしいものというよりは、一層役に立つもの
であることを望みたい。
いずれにせよ、経験豊かなプログラマは、本章の材料から自分のプログラミングに必要なも
のをすぐに応用できるはずである。初心者は自分で規模の小さな類似のプログラムを書いてみ
るとよいであろう。どちらの方も、第 2 章から始まるもっと詳しい説明を学ぶための概論とし
て本章を役立てていただきたい。
1.1 手始めに
新しいプログラミング言語を学ぶ唯一の方法は、それでプログラムを書いてみることである 。
最初に書くべきプログラムは、どんな言語でも同じで、例えば次のようなものである。
hello, world
という単語を印字せよ。
これが基礎となる障害物だとしよう。それを飛び越えるためには、どこかでプログラム・テ
キストをつくり、それを(上手にコンパイルし、)ロードし、ランし、出力がどこに行くかを
見いださなければならない。こういった機械的な細かなことをマスターすれば、他のことはす
べて比較的容易である。
Emacs-Lisp で、hello, world と印字するためのプログラムは、次のようになる。
(require 'stdio)
(defun main ()
(printf "hello, world\n"))
プログラムをどのようにして実行させるかは、どのシステムでも同じである。このプログラ
ムを Emacs の*scratch*バッファで走らせれば
hello, world
が出力として出てくるのである。
次に、このプログラム自体について少し説明しよう。一つの Emacs-Lisp プログラムは、その
大きさはどうであれ、関数と変数などからなる。関数には、行なうべき計算の過程を指示する
文、および計算で使われる値を格納する変数が含まれる。Emacs-Lisp の関数は Fortran のサブ
ルーチンや関数、あるいは Pascal の手続きや関数と似ている。上の例では、 main がそうした
関数になっている。普通は、関数には自分の好きな名前を付けて良い。 main は特別な名前では
なく、プログラムのどこかに main がなければならないというわけではない。
プログラムでは普通その仕事を行なうのに他の関数を呼び出すが、それは自分の書いたもの
であることもあれば、手元にあるライブラリの関数であることもある。
このプログラムの最初の行
(require 'stdio)
は、標準入出力ライブラリについての情報を含めるべきことを、Emacs に対し指示するもので
ある。この標準ライブラリについては、第 7 章および付録 B で説明する。
(require 'stdio)
;; 標準ライブラリについての情報を取り込む。
(defun main ()
;; 引数のない main という名の関数
(printf "hello, world\n")) ;; この文字列を印字するのにライブラリ関数
;; printf を main で呼ぶ。\n は改行記号。
最初の Emacs-Lisp プログラム
関数との間でデータをやりとりする唯一の方法は、呼び出しを行なう関数から相手の関数に
対して、引数と呼ばれる値のリストを渡すやり方である。関数名の次にあるカッコはこの引数
のリストを囲むのに使われる。この例では main は引数のない関数であることが()で示されて
いる。
この関数 main は次の文一つだけを含んでいる。
(printf "hello, world\n")
関数は名前で呼ぶが、その後に引数を並べたものを付ける。ここでは"Hello, World\n"という
引数を付けて、printf という名の関数を呼んでいる。printf は出力(この場合は引用符で囲ま
れた文字列)を印字する(標準出力バッファの終わりに書き込む自作の^^;)ライブラリ関数で
ある。
二重引用符"……"で囲まれる任意個の文字の列を、文字列と呼ぶ。さしあたり、われわれに
とっての文字列の唯一の使い方は、printf および他の関数の引数としてということになる。
文字列の中の\n という記号は、改行(newline)という文字の記号で、印字の場合、端末機
を次の行の左端に進めることを示す。この\n を省略すると(これはやってみるべき実験であ
る)、出力の後に改行が行なわれないことが分かるであろう。また、次のように書いても結果
は同じである。
(printf "hello, world
")
改行は printf では決して自動的には行なわれないから、1 行の出力は何回かの呼出しで段階
的に行なっても良い。最初のプログラムは次のように書いても、出力は同じである。
(defun main ()
(printf "hello, ")
(printf "world")
(printf "\n"))
ここで注意したいのは、\n が一つの文字を表わしているということである。\n のようなエス
ケープ符号列は、表現しにくい文字、すなわち目に見えない文字を表わす一般的で、拡張可能
な機構になっている。Emacs-Lisp で使う他の記号には、tab を表わす\t、バックスペースの
\b、二重引用符を表わす\"、バックスラッシュ自体を表わす\\がある。完全なリストは 2.3 節
に示す。
演習 1-1 手元のシステムでこのプログラムをランさせよ。プログラムの一部を省いて、どん
なエラー・メッセージが出るか、試みてみよ。
演習 1-2 x が上にあげなかったある文字であるとして、printf の引数に\x を含めるとどうな
るかを調べる実験を行なえ。
1.2 変数と算術
次のプログラムは、C=(5/9)(F-32)という公式を使って、華氏の温度と摂氏の温度の次のよう
な対応表を印字するプログラムである。
0
-17
20
-6
40
4
60
15
80
26
100 37
120 48
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148
このプログラム自体は、やはり main という名の一つの関数の定義からなる。これは hello,
world を印字するプログラムよりは長いが、複雑なものではない。この例では、コメント、宣
言、変数、算術式、ループ、書式付き出力といった、いくつかの新しいアイデアが使われてい
る。
(require 'stdio)
;; f=0,20,…,300 に対して、摂氏-華氏対応表
;; を印字する
(defun main ()
(let (fahr celsius lower upper step)
(setq lower 0)
; 温度表の下限
(setq upper 300)
; 上限
(setq step 20)
; きざみ
(setq fahr lower)
(while (<= fahr upper)
(setq celsius (/ (* 5 (- fahr 32)) 9))
(printf "%d\t%d\n" fahr celsius)
(setq fahr (+ fahr step)))))
最初の 1 行
;; f=0,20,…,300 に対して、摂氏-華氏対応表
;; を印字する
はコメントであり、この場合、このプログラムでやることを簡潔に説明するものとなってい
る。;(セミコロン)の後ろにあるこうした文章はインタプリタにより無視される。したがって、
コメントはプログラムを理解しやすくするのに自由に使うことができる。入れる場合は、空白
(blank)あるいは改行が書けるところならばどこでもよい。
Emacs-Lisp では、すべての変数は、一般的に使う前に、普通は関数の始めの実行可能文の前
のところで宣言しておかなければならないかというと、そうではない。let の最初のリスト
(fahr celsius lower upper step)は let フォームの中でローカルに使用したい変数名の羅列で
ある。ここでの変数はすべて数値を扱っており、取り得る値は-8388608〜8388607(24 ビット
実装)である。その他には、こういった基本型の配列、構造体、それらのシンボル、それらを
返す関数がある。これらについては後で説明する。
さて、前の温度換算プログラムの実際の計算は、代入文
(setq lower 0)
(setq upper 300)
(setq step 20)
(setq fahr lower)
で始まり、これで変数には初期値がセットされる。
表の各行は同じ式で計算されるから、出力行ごとに 1 回繰り返すループを使うのが良い。こ
れが次の while 文の目的である。
(while (<= fahr upper)
…
)
この while ループは次のように動く。まずカッコ内の条件をテストする。これが真(fahr が
lower 以下)であれば、ループの本体(while の次の 3 行)が実行される。次に条件が再チェッ
クされ、真であれば、ループの本体が再び実行される。このテスト結果が偽(fahr が upper を
超える)となると、ループは終りとなり、実行はこのループの後に続く文に移る。このプログ
ラムではそこから先には文がないから、そこで終りである。
while の本体は、温度換算プログラムでみたように 2 番目以降のリストである。while で制御
される文は tab ストップで字下げして、どの文がループの内部にあるかが一目でわかるように
する。字下げは、プログラムの論理構造を強調するのによい。Emacs-Lisp では文の位置はどこ
にあろうとまったくかまわないが、適当な字下げと空白のスペースを利用することは、プログ
ラムを見やすくするのに不可欠である。われわれがすすめたいのは、リストの記述をどのよう
に区切ってもよいので、必ずインデントコマンドを使用して字下げを行なうことだ。
さて、先の例では、仕事の大部分はループの本体で行なわれる。摂氏の温度を求めるのは次
の式である。
(setq celsius (/ (* 5 (- fahr 32)) 9))
ここで、単に(/ 5 9)を掛ける代わりに、5 を掛けてから 9 で割っている理由は、Emacs-Lisp で
は他の多くの言語と同様、整数の割算では切捨てが行なわれて、小数部が切り捨てられてしま
うからである。5 と 9 は整数だから、(/ 5 9)は切り捨てられてゼロになり、もちろんすべての
摂氏温度がゼロになってしまう。
この例は、printf がどう働くかをもう少しよく示す例にもなっている。printf は、実際には
汎用の書式変換関数である。これについては第 7 章で詳しく説明する。ここでは、最初の引数
は印字すべき文字列で、%記号はそれぞれの他の(第 2、第 3、……の)引数の一つをどこに代
入すべきか、それをどんな形で印字すべきかを示している。例えば、 %d は整数の引数を表わす
から
(printf "%d\t%d\n" fahr celsius)
という文は、二つの整数 fahr と celsius の値を、間にタブ(\t)をはさんだ形で印字せよとい
う指示になる。
printf の最初の引数では、おのおのの%記号は、それに対応する第 2、第 3 などの引数と対に
なっている。これらは数と型が正しくあっていなければならない。さもないとエラーになって
しまう。
ところで、printf は Emacs-Lisp の一部ではない。Emacs-Lisp に printf などという関数は定
義されていないからである。printf は単に、C 言語で通常使える標準のライブラリ・ルーチン
を模したものにすぎない。
ここでは Emacs-Lisp 自体に重点を置くために、入出力については第 7 章まであまり述べない
ことにする。
さて、この温度変換プログラムにはいくつかの問題点がある。まず簡単なほうからいうと、
数が右揃えになっていないために、出力が非常にきれいだとはいえないことである。しかし、
これを直すのは何でもない。printf 文の各%d に幅を付加すればよく、それで印字される数はす
のフィールド内で右詰めになる。例えば
(printf "%3d\t%6d\n" fahr celsius)
として、各行の最初の数字を 3 桁の幅のフィールドに、2 番目の数字を 6 桁幅のフィールドに印
字することにすれば、結果は次のようになる。
0
-17
20
-6
40
4
60
15
80
26
100
37
…
さらに重要な問題は、整数で計算を行なっているために、摂氏の温度があまり正確でないこ
とである。例えば、0F は実際には-17.8C であって-17C ではない。より正確な答えを得るには、
整数の代わりに、浮動小数点数演算を使うべきである。これを直すのも何でもない。次に示す
のは第 2 版である。
(require 'stdio)
;; f=0,20,…,300 に対して、摂氏-華氏対応表
;; を印字する; 浮動小数点数版
(defun main ()
(let (fahr celsius lower upper step)
(setq lower 0)
; 温度表の下限
(setq upper 300)
; 上限
(setq step 20)
; きざみ
(setq fahr lower)
(while (<= fahr upper)
(setq celsius (* (/ 5.0 9.0) (- fahr 32.0)))
(printf "%3.0f\t%6.1f\n" fahr celsius)
(setq fahr (+ fahr step)))))
これは前のプログラムとほぼ同じだが、変換の公式をより自然な形に直してある。前の版で
は、整数による演算の結果が切り捨てられてゼロになるために、(/ 5 9)が使えなかった。しか
しながら、定数の中の小数点は浮動小数点を表わすから、(/ 5.0 9.0)は切り捨てられない。こ
れは二つの浮動小数点数の比だからである。
算術演算では、被演算数が整数なら、整数演算が行なわれる。しかし、引数の片方が浮動小
数点数、他方が整数のときには、浮動小数点演算を行ない、結果を浮動小数点数値オブジェク
トに格納する。上のプログラムのように、整数値をもつことがわかっている場合でも、浮動小
数点定数に明示的に小数点を付けて書くのは、それが浮動小数点数であることを人間の読み手
に強調するためである。
次に printf の変換指示子%3.0f は、浮動小数点数を少なくとも 6 文字幅に、しかも小数点の
後は 1 桁にして印字するための指示である。これで出力は次のようになる。
0 -17.8
20
-6.7
40
4.4
…
変換仕様からは幅と精度は省いてもよい。 %6f は数が少なくとも 6 文字幅であることを表わ
す。%.2f は小数点の後が 2 文字だということを指示するだけで、幅には制限を付けない。%f は
数を浮動小数点数として印字せよというだけである。
%d
10 進数として印字
%6d
10 進数として印字、少なくとも 6 文字幅に
%f
浮動小数点数として印字
%6f
浮動小数点数として印字、少なくとも 6 文字幅に
%.2f
浮動小数点数として印字、小数点の後は 2 桁
%6.2f
浮動小数点数として印字、少なくとも 6 文字幅で、小数点の後は 2 桁
さらに、printf では、%o は 8 進、%x は 16 進、%c は文字、%s は文字列、%%は%自体という区別
が行なわれる。
演習 1-3 表の上に見出しを印字するように温度換算プログラムを変更せよ。
演習 1-4 温度を摂氏から華氏に換算するプログラムを書け。
1.3 Do 文
周知のように、プログラムを書くにはいろいろのやり方がある。温度換算プログラムを別の
形に書いてみよう。
(require 'cl)
(require 'stdio)
;; 摂氏-華氏対応表を印字する
(defun main ()
(do ((fahr 0 (+ fahr 20)))
((> fahr 300))
(printf "%3.0f\t%6.1f\n" fahr (* (/ 5.0 9.0) (- fahr 32.0)))))
これでも同じ答が出るが、プログラムは違って見える。大きく改めた点は、変数の多くを除い
たことで、fahr のみを残している。上限・下限ときざみの大きさは、do 文の定数としてのみ書
いた。この do はそれ自体が新しい構文である。また摂氏の温度を求める式は、別個の代入文に
する代わりに、printf の 3 番目の引数にしてある。
この最後の変更は、Emacs-Lisp におけるごく一般的な規則、すなわちある型の変数の値を使
うことが許される場合には、それがどこであれ、同じ型の式を使ってもよい、という場合の一
例である。printf の第 3 引数は、%6.1f に合う浮動小数点数値でなければならないから、任意
の浮動少数式がそこに書けるわけである。
さて、do 自体はループであり、while の一般化になっている。これを前に出てきた while と
比較すると、その役割は明白であろう。do には三つの部分がある。最初の
(fahr 0 (+ fahr 20))
は、ループの本体に入る前に一度だけ実行される。第二の
(> fahr 300)
という部分は、ループを制御するテストすなわち条件である。この条件を評価して、これが偽
であれば、ループの本体(ここでは単に printf)が実行される。次に、再初期化のステップ
(fahr 0 (+ fahr 20))
が実行され、先の条件が再び評価される。このループは条件が真となれば、終りとなる。while
と同様に、このループの本体は、一つの文であってもよいし、一群の文であってもよい。また
初期化と再初期化の部分は、一つの式なら何でも許される。
while と do はどちらを選んでもよく、わかりやすいほうを使えばよい。通例、 do のほうが
while よりもコンパクトで、ループを制御する文が一ヶ所にまとめて置けるので、初期化と再
初期化が一つの式であって、論理的に関係があるループには、do のほうが適している。
演習 1-5 温度換算プログラムに手を加えて、表を逆順に、すなわち 300 度から 0 度へという順
に印字するように直せ。
1.4 記号定数
温度換算の話を終える前に、一つ考えたいことがある。プログラムの中に 300 とか 20 という
"魔法の数(マジックナンバー)"を埋め込むのは悪い習慣である。これは、プログラムを後で
読まなければならない人には、たいした意味をもたないし、それを系統的に変えるのも困難で
ある。幸いにして、Emacs-Lisp にはそうした邪魔な数を避ける方法がある。プログラムの初め
に置く defconst 構文がそれで、これを使えば、次のように記号名すなわち記号定数として、特
定の文字列を定義することができる。
(defconst 名前 それと置き換えるべきテキスト)
その後、インタプリタでは、そうした名前が引用符なしに出てくると、それを対応する文字
列に置き換えるのである。置き換えられる文字列の方は、任意の文字列でよく、数値に限られ
ない。
(require 'cl)
(require 'stdio)
(defconst LOWER 0)
; 表の下限
(defconst UPPER 300)
; 上限
(defconst STEP
; ステップ・サイズ
20)
;; 摂氏-華氏対応表を印字する
(defun main ()
(do ((fahr LOWER (+ fahr STEP)))
((> fahr UPPER))
(printf "%3.0f\t%6.1f\n" fahr (* (/ 5.0 9.0) (- fahr 32.0)))))
ここで LOWER、UPPER、STEP という量は定数であって、変数ではないから、宣言しなくてよい。
記号名は、小文字の変数名と容易に区別できるように、大文字で書くのが普通である(?)。
1.5 文字入出力
次に、文字データに対し簡単な操作を行なう相互につながりのある一群のプログラムを考え
る。すぐわかるように、プログラムの多くは、ここで論ずるプロトタイプ(試作品)の拡張版
にすぎない。
標準入出力ライブラリによってサポートされる入出力のモデルは非常に簡単である。テキス
トの入力あるいは出力は、それがどこで発生したか、あるいはどこへ出力するかにはよらずに 、
文字のストリーム(流れ)として扱われる。このテキスト・ストリームは、行に分割された文
字列である。各行はゼロあるいはそれ以上の個数の文字の後に改行記号の付いた形をとる。各
入力あるいは出力のストリームをこのモデルに合うようにするのは、ライブラリの責任である 。
ライブラリを使う Emacs-Lisp プログラマは、プログラムの外で行がどう表現されているかにつ
いては心配する必要はない。
標準のライブラリには、一度に 1 文字を読み書きする関数が用意されている。getchar は、
呼ばれるごとにテキスト・ストリームから次の入力文字をもってきて、その値として、その文
字を返す。すなわち
(setq c (getchar))
の後では、変数 c には入力から次の文字が入る。文字は普通はキーボードから打ち込まれるも
のである。ファイルからの入力については第 7 章で述べる。
関数 putchar は呼ばれるごとに文字を出力するためのものである。
(putchar c)
は、通常、画面に整数変数 c の内容を示すのに使われる。putchar と printf の呼出しは、交互
に行なってもよい。出力は呼出しの行なわれた順に行なわれる。
1.5.1 ファイルの複写
getchar と putchar があれば、入出力についてそれ以外何も知らなくても、役に立つプログ
ラムをかなりたくさん書くことができる。最も単純な例は、入力を出力へ一度に 1 文字ずつ複
写(コピー)する次のプログラムである。
1 文字とってくる。
while(文字がファイルの終りではない)
読み込んだばかりの文字を出力
新しい文字をとってくる
これを Emacs-Lisp プログラムで表現すると、次のようになる。
(require 'stdio)
;; 入力を出力に複写;第 1 版
(defun main ()
(let ((c (getchar)))
(while (not (= EOF c))
(putchar c)
(setq c (getchar)))))
not は真偽値を反転させる。t なら nil を nil なら t を返す。
キーボードや画面の上で文字として現われるものは、他のすべてのデータと同じく、内部的
には単にビット・パターンとして格納される。そうした文字データを格納した変数は数値型に
なる(正確には、生成された数値オブジェクトにシンボル c がバインドされる。ただし数値は
シンボル内に値をもつことが可能であり、Emacs-Lisp ではそのように実装されている)。
EOF は stdio.el で定義されている整数である。しかし、それがどのキャラクタコードとも同
じでないものでありさえすれば、その具体的な数値は問題にする必要がない。記号定数を使う
ことにすれば、プログラムでは具体的な数値には何も左右されないことが保証される。
複写を行なうプログラムは、経験のある Emacs-Lisp プログラマなら実際にもっと短く書ける
であろう。Emacs-LIsp では
(setq c (getchar))
といった任意の代入文は、式の中に書ける。その値は、単に左辺に代入される値である。 c へ
の文字の代入を while のテスト部分に入れることにすれば、ファイルを複写するプログラムは
次のように書ける。
(require 'stdio)
;; 入力を出力に複写;第 2 版
(defun main ()
(let (c)
(while (not (= EOF (setq c (getchar))))
(putchar c))))
このプログラムでは、文字を一つとって c に代入した後、その文字がファイルの終りの目印
かどうかを while で調べている。目印でなければ、while の本体が実行され、その文字が印字
される。ついで、繰り返される入力が最後に終りになると、この while は終りとなり、したがっ
て main も終りとなる。
このプログラムは入力を中心にすえている──いまや getchar の呼出しは一つしかない──
から、短くなっている。テストの中に代入文を埋め込むことは、Emacs-Lisp に許される貴重な
圧縮記法の一つである。このスタイルはこれからもよく出てくる。(ただし、これを使いすぎ
ると、わかりにくいプログラムになってしまうので注意されたい。これはわれわれがなくした
いと思っている傾向である?)
演習 1-6 (= EOF (setq c (getchar)))という式の値が 0 か 1 であることを確認せよ。
不可能。=は t か nil を返す。
演習 1-7 EOF の値を印字するプログラムを書け。
1.5.2 文字のカウント
次のプログラムは文字数を数えるためのものである。これは複写のプログラムに少し手を加
えたものになっている。
(require 'stdio)
;; 入力される文字をカウント; 第 1 版
(defun main ()
(let ((nc 0))
(while (not (= EOF (getchar)))
(setq nc (1+ nc)))
(printf "%d\n" nc)))
次の文
(1+ nc)
には新しい関数 1+が出ているが、これは 1 だけの増分(インクレメント)を表わす。ここは(+
nc 1)と書いてもよいが、(1+ nc)のほうが短くて、しかもより一層能率的なことが多い(nc 変
数がレジスタに割り付けられればの話?)。これに対応する関数としては、 1 だけの減少(デ
クレメント)を表わす 1-がある。
ループを表現するもう一つの別のやり方を示すために、次に while の代わりに do を使って書
いてみよう。
(require 'stdio)
;; 入力される文字をカウント; 第 2 版
(defun main ()
(do ((nc 0 (1+ nc)))
((= EOF (getchar)) (printf "%d\n" nc))))
ここでは do ループの本体は空である。これはすべての仕事が、テストと再初期化の部分で行
なわれているからである。
この文字を数えるというプログラムを離れる前に注意しておきたいことは、入力に何も文字
がなければ、最初に getchar を呼ぶときに while のテスト結果なら偽、do のテスト結果なら真
となるから、このプログラムでゼロという当然の答が得られることである。while や do のいい
点の一つは、ループの本体に入る前に、その頭?のところでテストが行なわれることにある。
つまり、何もすることがなければ、ループの本体に入ることなく、何も行なわれないのである 。
"文字なし"というような入力がきたときでも、プログラムはちゃんと対応できるものでなけれ
ばならない。while 文と do 文は、境界条件においてもきちんとした処理が行なわれることを保
証しているといえる。
1.5.3 行数のカウント
次のプログラムは入力の行数を数えるものである。上に述べたように、標準ライブラリでは 、
各入力ストリームが、改行で区切られた一連の行の形になることが保証される。したがって、
行を数えるには、改行記号の数を数えればよい。
(require 'stdio)
;; 入力の行数をカウント
(defun main ()
(let (c (nl 0))
(while (not (= EOF (setq c (getchar))))
(if (= ?\n c)
(setq nl (1+ nl))))
(printf "%d\n" nl)))
この while の本体は、一つの if であり、これは、カウントアップ(setq nl (1+ nl))の制御
に使われている。この if 文では、カッコ内の条件を調べ、それが真であれば、次に続く文を実
行する。ここでも何が何を制御しているかを示すために字下げを行なった(字下げは Emacs が
適当にやってくれる)。
ここで等号=は、(Pascal の単一の=あるいは Fortran の.EQ.と同じく)"に等しい"ことを表
わす Emacs-Lisp の関数である。C 言語の初心者は==のつもりで=とつい書いてしまうことがあ
るが、Emacs-Lisp においてはそういうことはあり得ない。
次に、機械の文字セットの中において、文字の内部表現に等しい値を求めるには、単一の?に
続けて任意の 1 文字を書けばよい。これを文字定数と呼ぶが、これは小さな整数を書くもう一
つのやり方に他ならない。例えば、?A は文字定数である。ASCII 文字セットの中では、この値
は 65 であり、これが文字 A の内部表現になっている。もちろん、65 と書くよりは?A のほうが
よい。意味がはっきりするし、特定の文字セットとは独立だからである。
文字列で使われるエスケープ文字は文字定数の中でも有効である。したがって、判定文や式
の中でも、?\n は改行コードの値を表わす。注意すべきことは、?\n が一つの文字であることで、
式の中でこれは一つの整数と等価である。一方、"\n"はたまたま一つの文字だけを含む文字列
である。文字列と文字については、第 2 章でさらに論ずる。
演習 1-8 空白、タブ、改行を数えるプログラムを書け。
演習 1-9 二つ以上の空白を一つの空白に置き換えながら、入力を出力に複写するプログラム
を書け。
演習 1-10 各タブを\t に、各バックスペースを\b に、各バックスラッシュを\\に置き換えな
がら、入力を出力に複写するプログラムを書け。こうすれば、タブとバックスペースははっき
り目に見えるようになる。
バックスペースの可視化は処理してはいるが不可能。(これはダム端末の発想?^^;)
1.5.4 単語のカウント
次に、第四の有用なプログラムとして、行と単語と文字の個数を数えるプログラムを考えて
みよう。ただし、ここでは単語は、空白やタブや改行を含まない任意の文字列であるとゆるや
かに定義しておく。このプログラムは、UNIX のユーティリティ・プログラム wc の裸の骨格部
分である。
(require 'stdio)
(defconst IN
1)
(defconst OUT 0)
;; 入力中の行、単語、文字のカウント
(defun main ()
(let (c (nl 0) (nw 0) (nc 0) (state OUT))
(while (not (= EOF (setq c (getchar))))
(setq nc (1+ nc))
(if (= ?\n c)
(setq nl (1+ nl)))
(if (or (= ?
c) (= ?\n c) (= ?\t c))
(setq state OUT)
(if (= OUT state)
(progn
(setq state IN)
(setq nw (1+ nw))))))
(printf "%d %d %d\n" nl nw nc)))
このプログラムでは、単語の最初の文字がくるたびに、カウントを 1 進めている。変数
state は、現在見ているのが単語の中かどうかを示すもので、最初は "単語の中ではない"から、
値 OUT を割り当てておく。プログラムを読みやすくするうえからいうと、リテラル値 1 および 0
を使うよりは、記号定数 IN および OUT を使うほうが好ましいであろう。もちろん、こんな小さ
なプログラムではどちらでもいいことだが、大きなプログラムでは、わかりやすさということ
を考えると、はじめからこういう風に書くちょっとした労力は十分払うに値するのである。ま
た、数を記号定数としてだけ表わすようにしておけば、プログラムに広範な変更を加えるのが 、
よりたやすいこともわかるだろう。
次に
(let (c (nl 0) (nw 0) (nc 0) (state OUT))
という行は、let フォームのローカルバインドを初期化する。これは let の本体部分で、次の
ように書くのと同じである。
(setq nl 0 nw 0 nc 0)
一方、or は OR を意味するから
(if (or (= ?
c) (= ?\n c) (= ?\t c))
という行は、"c が空白あるいは c が改行あるいは c がタブならば"の意である。(エスケープ文
字列\t はタブ文字を目に見える形にした表現であることを思い出そう。)これに対応する AND
は and である。and でつながれた式は、順番に評価され、真偽が判明すると、ただちに評価を
止めることになっている。したがって、c が空白であれば、それが改行かタブであるかどうか
は調べる必要がないから、そのチェックは行なわれない。これはここでは重要なことではない
が、すぐにわかるように、もっと複雑な場合には非常に重要となる。
この文には、else 文も出てくる("else"の表記はないが、if の三番目のリストは else とし
て動作する)が、これは、if の条件式が偽のときに行なうべき代替行為を指定するものである 。
一般形式は次の形である。
(if (式 1)
式2
(式 3・・・)
if で指定される式 2、式 3 のうち、実行されるものはどちらか一方だけである。式 1 が真な
らば式 2 が実行され、そうでなければ式 3(暗黙の progn)が実行される。どちらの文ももっと
複雑にすることが可能である。先の単語数をカウントするプログラムは、三つ目の if で上記式
2 にあたる部分を progn を使って書いている。これは式 2 には一個のリストしか書けないからで
ある。
演習 1-11 単語カウント・プログラムのテストは、どのようにするか?もしバグがあるとした
ら、それをあばき出すにはどんな入力をすればよいか?
空白、改行、タブが 1 個、2 個、3 個以上連続するケースをそれぞれテストする。
(プログラムの制約によるが)入力にバックスペース文字を含むケースをテストする。
演習 1-12 入力した単語を 1 行に一つずつ印字するプログラムを書け。
演習 1-13 "単語の定義"をもっとはっきりさせて単語数をカウントするプログラムを書け。例
えば、英字で始まり、英字・数字・アポストロフィの連続したものを"単語”とするというよう
に。
1.6 配列
個々の数字と空白文字(ブランク、タブ、改行文字)とその他のすべての文字の出現する回
数を数えるプログラムを書いてみよう。これは少しわざとらしい例だが、この一つのプログラ
ムで Emacs-Lisp のいくつかの面を示すことができるからである。
入力に関してここでは 12 のカテゴリーがあるとするから、それぞれの数字の出現する回数を
把握するには 10 個の異なった変数を使うより、配列を使ったほうが便利であろう。ここに一つ
のプログラム形式を示す。
(require 'cl)
(require 'stdio)
;; 数字、空白、その他をカウント
(defun main ()
(let (c (nwhite 0) (nother 0) (ndigit [0 0 0 0 0 0 0 0 0 0]))
(fillarray ndigit 0)
(while (not (= EOF (setq c (getchar))))
(if (and (>= c ?0) (<= c ?9))
(aset ndigit (- c ?0) (1+ (aref ndigit (- c ?0))))
(if (or (= c ? ) (= c ?\n) (= c ?\t))
(setq nwhite (1+ nwhite))
(setq nother (1+ nother)))))
(printf "digits =")
(do ((i 0 (1+ i)))
((<= 10 i))
(printf " %d" (aref ndigit i)))
(printf ", white space = %d, other = %d\n" nwhite nother)))
自分自身に対するこのプログラムの出力は次のようになる。
digits = 18 5 0 0 0 0 0 0 0 1, white space = 149, other = 361
宣 言
(ndigit [0 0 0 0 0 0 0 0 0 0])
は ndigit が 10 個の配列であることを宣言している。Emacs-Lisp では、配列のインデックスは
常に 0 から始まるため、その要素は 0、1、…、9 になる。これは初期化と、配列の印字との do
ループにはっきり現われている。
インデックスは任意の整数式であり、もちろん i のような整数の変数とか定数を含んでもか
まわない。
この特定のプログラムは。数字の文字表現の性質に完全に依存するものである。
例えば、テスト
(if (and (>= c ?0) (<= c ?9))
では、c 中の文字が数字かどうか調べ、そうであれば、
(- c ?0)
でその数値が求まる。この式は?0、?1、…、?9 が連続して増加してゆく値をとるときにのみ有
効である。幸いこれは通常の文字セットすべてに当てはまる。
文字が数字か、空白文字か、その他か、を決定するのは次のシーケンスである。
(if (and (>= c ?0) (<= c ?9))
(aset ndigit (- c ?0) (1+ (aref ndigit (- c ?0))))
(if (or (= c ? ) (= c ?\n) (= c ?\t))
(setq nwhite (1+ nwhite))
(setq nother (1+ nother))))
パターン
(if 条件 1
文1
(if 条件 2
文2
文 3)
は多分岐の判断を表現する方法であるが、こういう場合は通常 cond を用いることが多い。この
プログラムはある条件が満足されるまで上から順に読まれ、条件が満足されたところでそれに
対応する文が実行され、それでこの構文はすべて終了となる。(もちろん文は復文であっても
よい。)もしいずれの条件も満足されなかった場合には、最後の文 3 が、存在すれば実行され
る。最後の文 3 が省略されていれば(単語数カウント・プログラムのように)何も実行されな
い。最初の if と最後の文との間にはいくつもの
(if 条件
文
というグループがあってもかまわない。スタイルとしては、長い判断文でもページの右側には
み出ることはないように、この構文を前記のように書くのが賢明である。
数多くの条件に分岐するもう一つの方法としては、第 3 章で述べる cond 文というのがある。
対照のために、第 3 章でこのプログラムの cond 版を示すことにする。
演習 1-14 入力した単語の長さをヒストグラム(度数分布図)にしてプリントするプログラム
を書け。ヒストグラムは横に書くほうが簡単だが、縦書きに挑戦してみるのもよい。
縦書きは当然パス(笑)。
1.7 関数
Emacs-Lisp の関数は、Fortran のサブルーチンあるいは関数、Pascal の手続きあるいは関数
に対応する(?)。関数はその中身に関して、何も心配しないで使えるようにある処理をブラック
ボックスの中につめ込むのに便利な方法である。関数は実際、大きなプログラムに潜在する複
雑さに対処するただ一つの方法となる。うまく設計された関数に関しては、いかに処理される
のかということを無視して、何が出来るのかを知っていれば十分である。Emacs-Lisp は関数の
使用が、やさしく、便利で、かつ能率的であるように設計されている。たった 2〜3 行しかなく
て、1 回しか呼ばれない関数を見かけることがよくあるが、それは断片的なプログラムをわか
りやすく、はっきりとさせるために使われているのである。
これまで、われわれは定義済みの標準入出力関数としては putchar、getchar、printf のみを
使った。そこで今度は自作の関数を二、三書いてみることにしよう。Emacs-Lisp には Fortran
にある**のような指数演算子がないから、整数 m を正整数 n でベキ乗する(power m n)という
館数を書いて、関数定義の機構を示すことにする。例えば、 (power 2 5)の答は 32 と出せるよ
うにしたい。この関数は小さな整数の正のベキ乗しか扱えないので、実用的なベキ乗ルーチン
ではないが、説明用には十分であろう。
以下に、関数 power とそれをテストする主プログラムを示す。全体の構造は一度に見ること
ができるであろう。
(require 'cl)
(require 'stdio)
;; ベキ乗関数をテストする
(defun main ()
(do ((i 0 (1+ i)))
((<= 10 i))
(printf "%d %d %d\n" i (power 2 i) (power -3 i)))
0)
;; power: base を n のベキ乗する; n >= 0
(defun power (base n)
(do ((i 1 (1+ i))
(p 1 (* p base)))
((< n i) p)))
どちらの関数も次のように同じ形をしている。
(defun function-name (parameter declarations, if any)
statements)
これらの関数はどんな順序で現われてもよく、一つのソース・ファイルにまとめても二つ以
上に分割してもかまわない。もちろんいくつかのソース・ファイルにした場合は、一つのとき
よりもロードに手間がかかる。しかしそれは言語の性質も問題ではなく、 OS の問題である(?)。
さしあたって今は、両方の関数は同一ファイル中にあるものと仮定する。このため Emacs-Lisp
プログラムの走らせ方についていままで学んだことは何一つ変わらない。
さて、関数 power は、次の行により main で 2 回呼び出されている。
(printf "%d %d %d\n" i (power 2 i) (power -3 i))
どちらの呼出しでも二つの引数を power に渡していて、その度に書式付きで印字されるべき整
数の値が返されている。式の中では、(power 2 i)は、2 や i が整数であるのと同様に整数であ
る。(すべての関数が整数の値をとるわけではない。これについては、第 4 章で取り上げる。)
power 自体の最初の行
(defun power (base n)
はパラメータの名前の宣言になっている。パラメータを表わすのに、power 内で使われる名前
は純粋に power にだけ使われる局所的なものであり、他の関数からアクセスすることはできな
い。したがって、他のルーチンでは、矛盾することなしに同じ名称を使うことができる。これ
は変数 i、p についてもそうであり、power 内の i と main 内の i は無関係である。
われわれは関数定義の中のカッコ内にリストされた変数を一般にパラメータと呼び、関数呼
出しに使われる値を引数(argument)という。同じ区別を行なうのに、仮引数および実引数とい
うことばが使われることもある。
power で計算した値 p は、do 文によって main に返される。返却値のところにはどんな式を書
いてもよい
((< n i) p)
関数からは必ずしも値を返す必要はないが、最後に評価した式の値が呼び出し側に返される。
これは関数の終わりのところで"端から落ちる”のと同様である。また呼出しを行なう関数側で
は関数の戻す値を無視することもできる。
main は他の関数と同じく関数であるから、その呼出し側に対して値を返すことができる。こ
の場合、呼出し側とは、そのプログラムが実行される環境である。通常、戻り値が 0 であれば
正常終了、ゼロでない値は異常な、あるいはエラーの終了状況を表わすのがわかりやすいが、
それは呼出し側の都合に合っていればよい(main 関数は最後に評価した式の結果を返す)。簡
単のため、これまでは、main 関数から終了状況を表わす値を返していなかったが、今後はそれ
を含めることにする。
演習 1-15 1.2 節の温度換算プログラムを、変換のための関数を使うように書き直せ。
1.8 引数──参照による呼出し (call by reference)
他の言語、特に C を使ったことのあるプログラマにとっては、Emacs-Lisp の関数には見慣れ
ない面があるかもしれない。Emacs-Lisp では、すべての関数の引数が"参照で"受渡しされるか
らである。これは呼び出された関数には、元の値(オブジェクト)が与えられることを意味す
る。このため、呼ばれたルーチンが元の値にアクセスできない C のような"call by value(値
による呼出し)"の言語とは、Emacs-Lisp の性質は違ったものになっている。
主な違いは、Emacs-Lisp では、呼ばれた関数が呼んだほうの変数を変えることができるとい
う点にある。これは変数がデータの器ではなく、シンボルとオブジェクトの関係になっている
ということであり、その振る舞いに注意する必要がある。シンボルについては第 5 章で詳しく
述べる。
1.9 文字配列
Emacs-Lisp における配列の最も一般的な形は、文字の配列である。文字配列の使い方とそれ
を操作する関数を示すために、一群の行を読み込んで、一番長い行をプリントするプログラム
を書いてみよう。プログラムの骨格は単純であり、次のようになる。
(while (別の行がある)
(if (以前に一番長かった行よりも長い)
その行を格納する
その長さを格納する))
一番長い行をプリントする
この骨格は、プログラムが自然にいくつかの断片に分かれることをはっきり示している。一
つの断片で新しい行を得、もう一つの断片でそれをテストし、もう一つのでそれを格納し、残
りので処理を制御するのである。
こうして全体がうまく分けられるから、この形でプログラムも書くことにしよう。まず最初
に、次の行を入力する関数 getline を書く。他のプログラムの関数としても有効なように、こ
の関数はできるだけ柔軟な形に書くことにする。getline では、最低でも、ファイルの終了を
表わす記号は返さなければならない。さらに一般的に有効であるようにプログラムを設計する
なら、行の長さを返し、ファイルの終わりでは 0 を返すようにする必要がある。0 というのは
決して正しい行の長さではないからファイルの終わりとして使ってよい。どの行にも最低 1 文
字あるはずだし、改行文字だけの行でも長さは 1 になるはずだからである。
次に、前に一番長かった行よりも長い行が見つかったら、それをどこかに格納しておかねば
ならない。ここで 2 番目の関数 copy が登場する。これは、その新しい行を安全なところへコピー
しておくためのものである。
最後に、getline と copy を制御する main プログラムが必要である。以上の結果を示すと次の
ようになる。
(require 'cl)
(require 'stdio)
(defconst MAXLINE 1000)
;; 最も長い入力行を印字する
(defun main ()
(let (line (max 0) longest)
(while (< 0 (car (setq line (getline MAXLINE))))
(if (< max (car line))
(progn
(setq max (car line))
(setq longest (copy (cadr line))))))
(if (< 0 max)
(printf "%s" longest))
0))
;; getline: 入力行と長さを返す
(defun getline (lim)
(let ((i 0) c (s (make-string 0 0)))
(while (and (< i (1- lim))
(not (= EOF (setq c (getchar))))
(not (= c ?\n)))
(setq s (concat s (char-to-string c)))
(setq i (1+ i)))
(if (= c ?\n)
(progn
(setq s (concat s (make-string 1 ?\n)))
(setq i (1+ i))))
(list i s)))
;; copy: 文字列をコピーする
(defun copy (string)
(copy-sequence string))
関数 getline および copy は一つのファイルに入っているとした。もし、別のファイルに入れ
るのであれば、そのファイル内で provide 宣言し、require で読み込むことになる。
getline では呼び出し元へ値を返すのにリストを使っている。最後の行(list i s)でリスト
を作成し、それが呼出し元へ返される。Emacs-Lisp の関数は最後に実行した結果が返されるの
である。
したがって Emacs-Lisp の関数は必ず何かの値を返すことになる。 copy の戻り値は copysequence によって作られる新たな文字列オブジェクトである。
このプログラムを離れるに当たって、こういった小さなプログラムには、幾分まずい点があ
ることをお断りしておく。例えば、行数の制限よりも大きな行を読み込んだときに main ではど
うすべきか?getline は文字数が MAXLINE になると入力を打ち切って、文字列の終わりに改行
を付加し、安全に動作する。改行を付加しなければ、main では返ってきた長さと最後の文字を
見て、行が長過ぎたかどうかを判断し、それによって対処することが可能である。われわれは 、
話を簡単にするため、この問題は無視した。
getline のユーザとしては、前もって入力行の長さを知る方法はないが、Emacs-Lisp の文字
列オブジェクトは予め領域を確保して作られるわけではないため、オーバーフローが起きるこ
とはない。copy において新たに作成される文字列オブジェクトも同様である。
演習 1-16 一番長い行を印字するプログラムの main ルーチンを書き直して、任意の長さの入
力行群の長さ、およびテキストの出来るだけ多くの部分を正しく印字できるようにせよ。
演習 1-17 80 文字より長い行をすべて印字するプログラムを書け。
(ロジックは同じなので、40 文字より長い行とする)
演習 1-18 各入力行から、行の後ろのプランクやタブを取り除き、かつ空白行はすべて削除す
るようなプログラムを書け。
演習 1-19 文字列 s を逆に並べる関数 copy-reverse を書け。さらに、この関数を使って、入力
を一時に 1 行ずつ逆転するプログラムを書け。
1.10 グローバル変数と適用範囲
main の中の変数(line、longest など)は、main に固有のものであり、局所的なものである。
というのは、それらは main 関数の内部で宣言され、以降の文脈でしか直接アクセス出来ないか
らである。他の関数内にある変数も同様である。例えば getline にある変数 i は、copy の i と
は何の関係もない。ルーチン内部の局所変数は、関数が呼ばれたときのみ存在し、関数から呼
出し元へ制御が戻れば消えてしまう。このような変数が他の言語にならって、通常、ローカル
変数とも呼ばれているのはこのためである。
ローカル変数は、関数の呼出しによって現れたり、消えたりするから、一度呼び出されてか
ら次に呼び出されるまでの間、それらの値は保持されない。そのため、それぞれの関数の入口
で、値をはっきりとセットしなければならない。セットしなければ、値は void となり、たとえ
その関数内であっても参照するとエラーになる。ローカル変数の代わりには、すべての関数に
対してグローバルな変数を宣言することが出来る。これはすなわち、任意の関数から名前によ っ
てアクセスできる広域変数である。(この機構は、Fortran の COMMON や一番外側のブロックで
定義された Pascal 変数に似ている。)グローバル変数は広域的にアクセスできるから、関数間
のデータのやりとりの目的に、引数リストの代わりに用いられる。さらに、グローバル変数は 、
関数が呼び出されたり終了したりするごとに現れたり消えたりするのではなく、永久的に存在
するため、それをセットした関数が用済みになった後もその値は残ったままである。
グローバル変数は任意の関数でも、その外側でも定義することができる。しかし、これもロ ー
カル変数同様、値がセットされていないうちは void のままであり、参照するとエラーになる。
議論を具体的にするために、line、longest、max をグローバル変数として、最も長い行を求め
る先のプログラムを書き直してみよう。これには三つの関数すべての呼出しを変更する必要が
ある(?)。
(require 'cl)
(require 'stdio)
(defconst MAXLINE 1000)
; 入力の最大行数
(defvar max)
; いままで出てきた最大長
(defvar line)
; 現在の入力行
(defvar longest)
; 格納されている最長行
;; 最も長い入力行を印字する ; 特別版
(defun main ()
(setq max 0)
(let (len)
(while (< 0 (setq len (getline)))
(and (< max len) (progn (setq max len)
(copy))))
(and (< 0 max) (printf "%s" longest)))
0)
;; getline: 入力行と長さを返す ; 特別版
(defun getline ()
(setq line (make-string 0 0))
(let ((i 0) c)
(while (and (< i (1- MAXLINE))
(not (= EOF (setq c (getchar))))
(not (= c ?\n)))
(setq line (concat line (char-to-string c)))
(setq i (1+ i)))
(and (= c ?\n) (progn (setq line (concat line (make-string 1 ?\n)))
(setq i (1+ i))))
i))
;; copy: 文字列をコピーする ; 特別版
(defun copy ()
(setq longest (copy-sequence line)))
main、getline、copy におけるグローバル変数は先の例の最初の数行で定義されている。構文
的には defvar を使用したものになっているが、これはわかりやすくするために記述したもので
あり、max、line、longest のいずれも最初に必要となったところで、いきなり使用しても問題
ない。
プログラムがいくつかのソース・ファイルに分かれていて、ある変数を、例えばファイル 1
で定義してファイル 2 とファイル 3 で使う場合、変数の使用を結び付けるのには、ファイル 2
とファイル 3 にあるプログラムが動き出す前に、ファイル 1 がロードされていればよい。
これまで、グローバル変数やローカル変数を参照するのに、宣言や定義という言葉を使用し
てきているが、これらは厳密には他の言語と意味が異なる。変数とは、その名前であるシンボ
ルと、それにバインドされるオブジェクト(値)を組み合わせたものである。Emacs-Lisp では
何かの変数を、例えば整数型として定義するということが出来ない。そうではなく、任意の数
値オブジェクトを、任意のシンボルにバインドすることにより整数を扱う変数として機能する
ようになる。つまり、何らかのルールで記憶域を予め割り当てておくということが出来ないの
だ。
ところで、やりとりが簡単に思える──引数リストが短く、必要なときには変数がいつも存
在している──ためか、見えるものすべてをグローバル変数にしてしまおうとする傾向がある。
しかしグローバル変数は、必要のないときでも常に存在しているものである。このコーディン
グ形式には危険な落とし穴がいくつかある。というのは、データ結合の不明確なプログラムを
つくったり、そのため変数が予期しないときとか、うっかりしたときに変更されてしまったり 、
必要が生じても後からは変更しにくいプログラムになってしまったりすることがあるからであ
る。一番長い行を印字するプログラムの第 2 版は、この意味においても、また、操作する変数
の名前を組み入れることで、二つの非常に有用な関数の汎用性を壊しているという意味におい
ても、最初の版に比べて劣っている。
さて、ここまでで、Emacs-Lisp で普通に使われる中核部分とでも呼ぶべきところはカバーし
た。いままで述べた一握りのビルディング・ブロックを使えば、かなりな大きさの実用的なプ
ログラムを書くことができるから、ここで少し休んで、十分よく考えてから先に進むのが賢明
であろう。次の演習問題は、本章の始めのほうで示したプログラムよりもかなり複雑なプログ
ラムを想定している。
演習 1-20 入力されたタブを、次のタブ・ストップまでのスペースをうめる適当な数のブラン
ク(空白)で置き換えるプログラム detab を書け。タブ・ストップの位置は、例えば n 文字ご
とというように固定して考えよ。n は変数にすべきか、記号パラメータにすべきか?
(タブ・ストップは Emacs 上で認識されるため意識しないことにする)
演習 1-21 
Fly UP