defun
.
.emacs
file.
--- The Detailed Node Listing ---
Preface
List Processing
Lisp Lists
The Lisp Interpreter
Evaluation
Variables
Arguments
Setting the Value of a Variable
setq
to count.
Practicing Evaluation
How To Write Function Definitions
defun
special form.
interactive
.
Install a Function Definition
Make a Function Interactive
let
The if
Special Form
if
expression.
save-excursion
A Few Buffer--Related Functions
goto-char
,
point-min
, and push-mark
.
beginning-of-buffer
.
save-excursion
and
insert-buffer-substring
.
The Definition of mark-whole-buffer
The Definition of append-to-buffer
let
expression.
save-excursion
works.
A Few More Complex Functions
set-buffer
, get-buffer-create
.
or
.
goto-char
,
point-min
, and push-mark
.
The Definition of insert-buffer
or
and a let
.
if
instead of an or
.
or
expression works.
save-excursion
expressions.
The Interactive Expression in insert-buffer
Complete Definition of beginning-of-buffer
beginning-of-buffer
with an Argument
Narrowing and Widening
save-restriction
special form.
car
, cdr
, cons
: Fundamental Functions
cdr
repeatedly.
cons
Cutting and Storing Text
zap-to-char
progn
function.
point
and search-forward
.
The Version 18 Implementation
progn
expression
copy-region-as-kill
copy-region-as-kill
The Body of copy-region-as-kill
kill-append
function
copy-region-as-kill
How Lists are Implemented
Yanking Text Back
kill-ring-yank-pointer
variable.
Loops and Recursion
while
while
loop that uses a list.
while
, car
, cdr
.
A Loop with an Incrementing Counter
Loop with a Decrementing Counter
Recursion
while
loop with recursion.
Recursion in Place of a Counter
Regular Expression Searches
sentence-end
.
search-forward
.
TAGS
table.
forward-sentence
while
loops.
forward-paragraph
: a Goldmine of Functions
let*
expression.
while
loop.
forward-paragraph
code.
Counting: Repetition and Regexps
The count-words-region
Function
count-words-region
Counting Words in a defun
count-words
.
Count Words in defuns
in Different Files
Prepare the Data for Display in a Graph
Readying a Graph
Your .emacs
File
.emacs file
.
Debugging
Handling the Kill Ring
The rotate-yank-pointer
Function
rotate-yank-pointer
.
The Body of rotate-yank-pointer
if
expression.
%
, function.
%
in rotate-yank-pointer
.
yank
rotate-yank-pointer
.
A Graph with Labelled Axes
let
expression in print-graph
.
The print-Y-axis
Function
The print-X-axis
Function
Printing the Whole Graph
Graphing Numbers of Words and Symbols
本書は、「Programming in Emacs Lisp: An Introduction」
(texinfoファイルemacs-lisp-intro.texi
、1.05版、更新日1997年10月21日)
の翻訳である。
日本語訳ファイルは
http://www.ascii.co.jp/pub/GNU/emacs-lisp-intro-jp.texiに置いてある。 日本語のTeXが利用できる環境ならば、
texi2dvi
などのコマンドで
自前でdviファイルを作成して印刷できる。
また、Muleを用いてバッファに読み込んでから
M-x texinfo-format-buffer
を実行すれば、
日本語版のinfoファイルを作成することもできる。
ただし、このようにして作ったinfoファイルでは、
(メニューなどの)info特有の部分は未翻訳であることをあらかじめご了承願いたい。
Mule(Multilingual Enhancement to GNU Emacs)とそのLispは、
多国語を扱うために拡張したため、
GNU EmacsやEmacs Lispと互換ではない部分がある。
詳しくはMuleのディストリビューションに含まれるmule-jp.texi
や
マルチリンガル環境の実現
1
(特に第3章「Muleによるマルチリンガル環境の実現」)を参照してほしい。
なお、本書の例題は、英数字を使用する限りはそのまま動作する。
GNU Emacsが扱う(7ビットの)ASCIIコードの文字は、 コンピュータ内部では1文字を1バイトで表現し、 これらの文字は画面上の表示でも1文字あたり1コラムを占める。 したがって、文字数、内部表現のバイト数、表示コラム数のどれを とっても同じ値である。 また、テキストをファイルに収めたときの文字コードと GNU Emacsのバッファ内での文字コードはまったく同じである。
ところが、多国語を扱うMuleでは、こうはならない。 各国ごとにその国の文字を1バイトあるいは2バイトで表す規格がある。 たとえば、日本には文字コードの規格としてJIS X 0208があり 2バイトで1文字を表す。 また、ローマ字とカタカナの規格としてJIS X 0201があり 1バイトで1文字を表す。
1つのバッファ内に複数の国の文字コードが混在してもよいように、 Muleでは1バイトないしは2バイトの文字コードのまえに識別用に 1バイト(ないしは2バイト)を付加してバッファ内に保持する。 この識別用のバイトをリーディングキャラクタ(leading character)と呼ぶ。 ただし、7ビットのASCIIコードの文字は、GNU Emacsと同じで、 リーディングキャラクタは付加しない。
たとえば、日本語の「あ」は、文字数は1であるが、 内部表現には3バイト必要であり、表示には2コラム必要である。 半角の「ア」は、文字数は1であるが、内部表現には2バイト必要であり、 表示には1コラム必要である。 ASCII文字の「a」は、文字数も内部表現のバイト数も表示コラム数も1である。
このため、Muleでは、関数length
(See length)は、
文字列の「文字数」ではなく、
内部表現の「バイト数」を返す。
バッファ内のポイントやマーク
(See Buffer Size & Locations)
の位置も、「文字数」ではなく内部表現の「バイト数」が単位である。
一方、関数forward-char
は、その名前から予想されるように、
文字単位でポイントを移動する。
文字列の「文字数」を調べるには関数chars-in-string
を使い、
「表示コラム数」を調べるには関数string-width
を使う。
(length "abcあいう") => 12 (chars-in-string "abcあいう") => 6 (string-width "abcあいう") => 9
.emacs
(See Emacs Initialization)で
Muleの版を区別するには、関数mule-version
が返す文字列を使う。
また、日本語入力に「Wnn」、「sj3」、「かんな」のどれが使えるかを判別し、
それぞれに固有の設定を行うには、
関数featurep
を使ってつぎのようにする。
(if (and (not (featurep 'egg)) (featurep 'canna)) (progn 「かんな」に固有の設定 ... )) (if (and (featurep 'egg) (featurep 'wnn-egg)) (progn 「WNN」に固有の設定 ... )) (if (and (featurep 'egg) (featurep 'sj3-egg)) (progn 「sj3」に固有の設定 ... ))
MuleがX Window Systemのクライアントとして動作している場合、
変数window-system
には値x
が、
そうでない場合にはnil
が束縛されるので、つぎのように判別できる。
(if (eq window-system 'x) (progn 「X Window System」に固有の設定 ...) 画面端末の場合の設定 ...)
テキストエディタGNU Emacsの大部分はEmacs Lispと呼ばれるプログラミング 言語で書かれている。 このプログラミング言語で書いたコードは、ユーザーが指令を与えた ときに何をすべきかをコンピュータに指示するソフトウェア(一連の命令)である。 Emacsは、Emacs Lispで新たにコードを書いてエディタの拡張機能として 簡単に追加できるように設計されている。 「拡張可能なエディタ」と呼ばれる結縁である。
(Emacsは編集機能以上のものを提供するため、「拡張可能な計算環境」とでも 呼ぶべきであるが、少々いいづらい。 マヤ暦や月の満ち欠けを調べたり、多項式を簡約化したり、コードをデバッグしたり、 ファイルを管理したり、手紙を読んだり、本を書いたりなどの Emacsで行えるすべてのことは、もっとも一般的な意味で編集である。)
Emacs Lispはテキストエディタに関連付けて考えられがちであるが、 それ自体で1つのプログラミング言語である。 他のプログラミング言語と同様に使える。
プログラミングを理解したい、Emacsを拡張したい、プログラマになりたいという 読者もいることであろう。 Emacs Lispの入門である本書は、プログラミングの基本を学ぶための指針を与え、 さらに重要なことは、自力で学習する方法を示すために執筆したものである。
本書には、Emacsで実行できる小さな例題プログラムがある。 GNU EmacsのInfoで読んでいる場合には、 例題プログラムに出会うたびにそれらを実行できる (これは簡単に実行できるが、その方法は例題をあげたときに説明する)。 あるいは、Emacsが動いているコンピュータを脇に置いて、 印刷された本書を読んでいる場面もあろう (これは、筆者の好みでもある。 筆者は印刷した書物が好きである)。 手もとでEmacsを実行できなくても本書を読む意味はある。 ただし、そのような場合には、小説や初めての国への旅行案内とみなしてほしい。 興味深いはずであるが、実際にそこにいるのとは異なる。
本書の大部分は、GNU Emacsで使っているコードを眺める、 つまり、ウォークスルーであり、ガイド付きツアーである。 これらのツアーには2つの目的がある。 第一に、実際に動作する(日常的に使用している)コードに慣れてもらうことであり、 第二に、Emacsの動作方式に慣れてもらうことである。 エディタの実装方式を学ぶことは興味深いはずである。 また、ソースコードを読み進む際のコツも学んでほしい。 ソースコードから学んだり、アイデアを堀り起こせるはずである。 GNU Emacsはまさに宝の山である。
エディタとしてのEmacsやプログラミング言語としてのEmacs Lispを学ぶことに加えて、
例題やガイド付きツアーは、Lispのプログラミング環境としてのEmacsを熟知する
機会となるはずである。
GNU Emacsは、プログラミングの支援に加えて、M-.
(コマンドfind-tag
を起動するキー)などの慣れると便利なツールも提供する。
エディタ環境の一部であるバッファやその他のオブジェクトについても学ぶ。
Emacsのこれらの機能を学ぶことは、読者の街の周りの道を新たに学ぶことに似ている。
読者が知らないプログラミングの側面を学ぶためのEmacsの利用法も伝えたいと思う。 読者を惑わすことがらを理解したり、新たなことを行う方法を調べるためにも Emacsを利用できるのである。 この独立性は好ましいだけでなく利点でもある。
本書は、プログラマではない人向けの初歩の入門書である。 すでにプログラマである読者には、本書は物足りないであろう。 というのは、そのような読者はすでにリファレンスマニュアルを存分に読めるように なっており、本書の構成は間延びして見えるであろう。
経験あるプログラマは、本書をつぎのように評価してくれた。
リファレンスマニュアルで学ぶほうが好きである。 各段落に「飛び込んで」、段落のあいだで「息つぎ」する。段落を読み終えたときには、そこで取り上げた話題は完結しており、 必要なことは(以降の段落でより詳しく説明する場合を除いて) すべてわかったと仮定したい。 よく構成されたリファレンスマニュアルには、 冗長な部分がなく、必要な情報への索引が整備されているはずである。
本書は、このような人向けではない!
第一に、おのおののことがらを少なくとも3回は説明するように努めた。 1回目は紹介、2回目は使い方、3回目は別の使い方や復習である。
第二に、1つの話題に関するすべての情報を1か所にまとめることはせずに、 1つの段落に詰め過ぎないようにした。 筆者の考え方では、そうしないと読者に重荷を背負わせることになるからである。 かわりに、その時点で必要なことのみを説明するように努めた (あとで詳しく説明する場合に備えて、少々余分に説明する場面もある)。
1回読むだけで、すべてを理解してもらえるとは考えていない。 読者は、説明内容を「わかったつもり」にしておく必要があるだろう。 重要なことがらを正しく読者に指し示し、 注意を促すように本書を構成したつもりである。
いくつかの段落には、「飛び込んで」もらうしかなく、 それ以外に読み進む方法はない。 しかし、そのような段落の個数は少なくするように努めた。 本書は登頂困難な山ではなく、楽に歩ける小山である。
本書Emacs Lispプログラミング入門には、 姉妹編と呼ぶべきドキュメント がある。 リファレンスマニュアルには本書より詳しい説明がある。 リファレンスマニュアルでは、1つの話題に関する情報は1か所にすべてまとめてある。 上で述べたようなプログラマは、そちらを参照すべきである。 もちろん、本書を読み終えて自分のプログラムを書くときには、 リファレンスマニュアルが有用であるはずである。
Lispは、人工知能の研究のために、 1950年代末にマサチューセッツ工科大学で初めて開発された。 Lisp言語はその強力な機能のため、 エディタコマンドを書くなどの他の目的にも優れている。
GNU Emacs Lispは、1960年代にMITで開発されたMaclispから多くを受け継いでいる。
1980年代に標準規格となったCommon Lispからも一部を受け継いでいる。
しかし、Emacs Lispは、Common Lispよりもずっと単純である
(Emacsの標準ディストリビューションには、
Common Lispの多くの機能をEmacs Lispに追加するための
機能拡張用ファイルcl.el
がある)。
GNU Emacsを知らない読者にも、本書は有益であろう。 しかし、たとえスクリーンの移動方法だけであってもEmacsを学ぶことを勧める。 Emacsの使い方は、オンラインのチュートリアルで自習できる。 それには、C-h tとタイプする (つまり、<CTRL>キーとhを同時に押してから離し、 さらに、tを押してから離す)。
Emacsの標準コマンドを参照するために、
M-C-\(indent-region
)のように、
コマンドを起動するために押すキーに続けて括弧内にコマンド名を書く。
つまり、コマンドindent-region
は、
慣習的にはM-C-\とタイプすると起動できることを意味する
(望むならば、コマンドを起動するためのキーを変更することもできる。
これをリバインド(rebinding)という。
See Keymaps)。
M-C-\は、<META>キー、<CTRL>キー、<\>キーの3つを
同時に押すことを意味する。
ピアノを演奏するときの和音になぞらえて、
このような組み合わせをキーコード(keychord)と呼ぶこともある。
<META>キーがないキーボードでは、かわりに<ESC>キーを前置キーとして使う。
このような場合には、M-C-\は、<ESC>キーを押して離してから、
<CTRL>キーと<\>キーの2つを同時に押すことを意味する。
GNU EmacsのInfoで読んでいる場合には、 スペースバー<SPC>を押すだけで全体を読み進められる (Infoについて学ぶには、C-h iとタイプしてInfoを選択すればよい)。
用語に関しての注意:単語Lispのみを使った場面では Lispのさまざまな方言全般を意味するが、 Emacs Lispを使った場面ではGNU Emacs Lispのみを意味する。
本書の執筆に協力していただいた方々に感謝したい。 特に、Jim Blandy、Noah Friedman、Jim Kingdon、Roland McGrath、 Frank Ritter、Randy Smith、Richard M. Stallman、 Melissa Weisshausに感謝したい。 辛抱強く励ましてくれたPhilip JohnsonとDavid Stampeにも感謝したい。 誤りがあれば筆者の責任である。
慣れていない人の目には、Lispは不可思議なプログラミング言語である。
Lispのコードには、いたるところに括弧がある。
「Lots of Isolated Silly Parentheses(奇妙な括弧が多い)」の略であると
批判する人達もいる。
しかし、この批判は不当である。
LispはLISt Processingの略であり、括弧で囲んだリスト(list)
(および、リストのリスト)を扱うプログラミング言語である。
括弧はリストの境界を表す。
リストの直前にアポストロフィ、つまり、引用符'
を付ける場合もある。
リストはLispの基本である。
Lispでは、リストを'(rose violet daisy buttercup)
のように書く。
このリストの直前にはアポストロフィが1つ付いている。
これはつぎのように書くこともでき、
こちらの書き方にも慣れてほしい。
'(rose violet daisy buttercup)
このリストの各要素は異なる4つの花の名前である。 個々の要素を空白で区切り、庭の花を石で囲うように括弧で囲む。
(+ 2 2)
のように、リストには数が含まれてもよい。
このリストには、プラス記号+
に続けて2つの2
があり、
おのおのは空白で区切ってある。
Lispでは、データとプログラムのどちらも同じ方法で表現する。 つまり、どちらも、単語や数やリストを空白で区切って括弧で囲んだリストである (プログラムはデータのようにも見えるので、 プログラムは容易に他のプログラムのデータとなりえる。 これは、Lispの強力な機能である)。
(原文の2つの括弧書き
(Since a program looks like data, one program may easily serve as data for another; this is a very powerful feature of Lisp.)や
(Incidentally, these two parenthetical remarks are not Lisp lists, because they contain;
and.
as punctuation marks.)
には、句読点記号として;
や.
が含まれるので
Lispのリストではない。)
つぎもリストの例であり、リストの中にリストを含む。
'(this list has (a list inside of it))
このリストの要素は、this
、list
、has
の単語と、
リスト(a list inside of it)
である。
内側のリストは、a
、list
、inside
、of
、
it
の単語からできている。
Lispでは、これまで単語と呼んできたものをアトム(atoms)と呼ぶ。
この用語はアトム(原子)の歴史的な意味からきており、
「不可分」ということである。
Lispに関していえば、リストに用いてきた単語はそれ以上には小さく分割できず、
プログラムの一部として同じものを意味する。
数や+
のような1文字の記号についてもそうである。
一方、アトムとは異なり、リストは部分に分割できる
(See car cdr & cons)。
リストでは、アトムを空白で区切る。 アトムは、括弧のすぐ隣にあってもよい。
技術的には、Lispのリストは、空白で区切ったアトムを囲む括弧、
リストを囲む括弧、アトムやリストを囲む括弧から成る。
リストは、たった1個のアトムを含むだけでも、まったく含まなくてもよい。
何も含まないリストは()
のように書き、空リスト(empty list)と呼ぶ。
空リストは、それ以外のものとは異なり、アトムであると同時にリストでもある。
アトムやリストを表示したものをシンボリック式(symbolic expressions)、 あるいは、より簡素にはS式(s-expressions)と呼ぶ。 用語「式(expression)」そのものでは、表示したアトムやリスト、あるいは、 コンピュータ内部に格納したアトムやリストを意味する。 しばしば、これらを区別せずに用語「式(expression)」を使う (さらに、多くの書籍では式の同義語として用語「フォーム(form)」を使う)。
われわれの宇宙を構成するアトム(原子)は、 それらが不可分であると考えられた時代に命名されたものであるが、 物質原子は不可分ではないことが知られている。 原子の一部を分割したり、ほぼ同じ大きさに分裂させたりできる。 物質原子は、その真の性質が発見される以前に命名されたのである。 Lispでは、配列などのある種のアトムは構成部分に分割できるが、 この分割機構はリストを分割する機構とは異なる。 リスト操作に関する限り、リストのアトムは分割できない。
英語の場合と同様に、Lispのアトムを構成する文字は、
単語を作り上げる個々の文字とは異なる。
たとえば、南米のナマケモノを表す単語ai
(ミツユビナマケモノ)は、
2つの単語a
とi
とはまったく異なる。
自然界には多種類の原子が存在するが、Lispには数種類のアトムしかない。
たとえば、37、511、1729などの数(numbers)、
+
、foo
、forward-line
などの
シンボル(symbols)である。
これまで例にあげた単語はすべてシンボルである。
Lispの日常の習慣では、用語「アトム」をあまり使わない。
というのは、プログラマは扱っているアトムの種類を特定しようとするからである。
Lispのプログラミングでは、リスト内のシンボル(やときには数)を扱う
(括弧書き「(やときには数)」の原文(and sometimes numbers)
は、
アトムを空白で区切って括弧で囲んであり、
しかも、Lispの句読点記号以外は含まないのでLispのリストである)。
さらに、二重引用符で囲まれたテキストは(文であろうと段落であろうと) アトムである。 つぎに例を示す。
'(this list includes "text between quotation marks.")
Lispでは、句読点記号や空白を含みこのように囲まれたテキストは単一のアトムである。 この種のアトムは文字列(string)と呼ばれ、 コンピュータが人間向けに出力するメッセージに使う。 文字列は、数やシンボルとは異なる別の種類のアトムであり、使い方も異なる。
リスト内の空白の個数はいくつでもよい。 Lisp言語の視点からすれば、
'(this list looks like this)
は、つぎとまったく同等である。
'(this list looks like this)
どちらの例もLispにとっては同じリストであり、
this
、list
、looks
、like
、
this
のシンボルからこの順に構成されたリストである。
余分な空白や改行は人間がリストを見やすくするためのものである。 Lispが式を読み取るとき、余分な空白をすべて取り除く (ただし、アトムとアトムのあいだには、 これらを区切るために少なくとも1つの空白が必要である)。
奇妙に思えるかもしれないが、これまでの例で、 Lispのすべてのリストがどのようなものであるかを見てきた。 Lispのリストは多かれ少なかれこれまでの例のようなものであり、 もっと長かったり複雑なだけである。 要約すれば、リストは括弧で囲まれたものであり、 文字列は二重引用符で囲まれたものであり、シンボルは単語のようなものであり、 数は数字列である (鈎括弧やドットや特別な数種の文字を使う場合もあるが、 しばらくはこれらを使わない)。
GNU EmacsのLisp InteractionモードやEmacs LispモードでLispの式を 入力する場合には、Lispの式を読みやすく整形するためのコマンドを利用できる。 たとえば、<TAB>キーを押すと、カーソルを置いた行を自動的に字下げする。 あるリージョンのコードを正しく字下げするコマンドは、 慣習的にM-C-\にバインドされている。 リストの各要素が、どのリストに属するかがわかりやすくなるように字下げする。 つまり、内側のリストの各要素は、 そのリストを囲むリストの要素よりも字下げされる。
さらに、閉じ括弧をタイプするとEmacsは対応する開き括弧に一時的に カーソルを移動して、対応関係がわかるようにする。 Lispに与える個々のリストは括弧の対応が取れている必要があるので、 これはとても便利である (Emacsのモードに関して詳しくは、 See Major Modes)。
Lispのどんなリストも実行できるプログラムである。 それを実行する(Lispの専門用語では評価(evaluate)する)と、 コンピュータは、つぎの3つのうちの1つを行う。 リストそのものを返して、それ以外のことは何もしない。 エラーメッセージを出す。 リストの先頭シンボルをなんらかのコマンドとして扱う (もちろん、普通は3番目の動作を望む!)。
前節の例においてリストの直前に付けた1つのアポストロフィ'
を
クオート(quote)と呼ぶ。
リストの直前にこれを付けると、
そのリストに関しては何もせずに字面どおりに扱うことをLispに指示する。
リストの直前にクオートがない場合には、リストの先頭要素を特別扱いし、
コンピュータが従うべきコマンドとなる
(Lispでは、これらのコマンドを関数(functions)と呼ぶ)。
まえにあげたリスト(+ 2 2)
の直前にはクオートがないので、
Lispは、+
がリストの残りの部分に対して行うべき操作であると解釈する。
この場合、後続の数を加算する。
GNU EmacsのInfoで読んでいる場合には、つぎのようにしてリストを評価する。 つぎのリストの閉じ括弧の直後にカーソルを置いてから C-x C-eとタイプする。
(+ 2 2)
エコー領域に数4
が表示されるはずである
(専門用語では、たったいま「リストを評価」したのである。
エコー領域とは画面の最下行のことで、テキストを表示する場所である)。
クオートしたリストについても同じことをやってみよう。
つぎのリストの直後にカーソルを置いてC-x C-eとタイプする。
'(this is a quoted list)
今度は、エコー領域に(this is a quoted list)
と表示されるはずである。
いずれの場合も、GNU Emacsの内部にある Lispインタープリタ(Lisp interpreter)と呼ばれるプログラムに指令、 つまり、式を評価せよという指令を与えたのである。 Lispインタープリタという名称は、表現の意味を追いかける人間の仕事、 つまり、通訳(interpreter)からきている。
リストの一部ではないアトム、つまり、 括弧に囲まれていないアトムを評価することもできる。 この場合もLispインタープリタは人間が読める表現をコンピュータの言語に変換する。 このこと(see Variables)を説明するまえに、 まちがいを起こした場合にLispインタープリタがどうするかを説明しよう。
偶然引き起こした場合は気にしないでよいが、ここでは エラーメッセージを生成するようなコマンドをLispインタープリタに与えてみる。 これは無害であり、意図してエラーメッセージを生成することにある。 専門用語を理解すれば、エラーメッセージは有益でさえある。 「エラー」メッセージと呼ぶよりは「ヘルプ」メッセージと呼ぶべきであろう。 これらは、見知らぬ国を訪れた旅行者のための道標のようなものである。 読みこなすのはたいへんであろうが、 いったん理解してしまえば道を指し示してくれる。
リストの先頭要素に意味のあるコマンドもなく、クオートもしていないリストを 評価してみよう。 上で用いたリストとほとんど同じであるが、その直前には引用符を付けない。 このリストの直後にカーソルを置いてC-x C-eとタイプする。
(this is an unquoted list)
今度は、エコー領域につぎのようなメッセージが表示されるはずである (端末のベルが鳴ったり画面が点滅したりする場合もある。 これは人間の注意を引くためのものである)。
Symbol's function definition is void: this
カーソルを移動したり、何かキーをタイプするだけでメッセージは消えてしまう。
既知のことをもとにすれば、このエラーメッセージをほぼ読み取れる。
用語Symbol
の意味は知っている。
ここでは、リストの先頭要素である単語this
を意味する。
用語関数(function)
については一度述べた。
これは重要な用語である。
ここでは、関数(function)とは、コンピュータに何かを行わせるための
コンピュータに対する一連の命令であると定義しよう
(技術的には、シンボルは一連の命令を探す場所を指定するのであるが、
ここではその詳細は無視できる)。
これでエラーメッセージSymbol's function definition is void: this
の意味を理解できるはずである。
シンボル(つまり単語this
)には、
コンピュータが実行すべき、いかなる命令列も定義されていないのである。
メッセージfunction definition is void
の単語の使い方が
少々変わってるのは、Emacs Lispの実装方法に準じているのである。
つまり、シンボルに関数定義が与えられていない場合には、
命令列を格納する場所は「void(空)」になるのである。
一方で、(+ 2 2)
を評価すると2に2を加算できるので、
シンボル+
にはコンピュータが実行すべき命令列が与えられており、
しかも、それらは+
に続く数を加算する命令であると推理できる。
これまでに説明してきたことをもとに、Lispの別の特徴を明確にすることができる。
+
のようなシンボルは、それ自身はコンピュータが実行すべき命令列
ではないという重要な特徴である。
そのかわりに、定義、すなわち、命令列を探すために
シンボルを(一時的に)使うのである。
われわれが見ているものは、命令列を探すための名前である。
人の名前も同じように使われる。
筆者はBob
と呼ばれているが、私はB
、o
、b
の3文字
ではなく、意識のある生命体である。
名前そのものは私ではなく、私を指すために名前を使うのである。
Lispでは、1つの命令列に複数の名前を結び付けることができる。
たとえば、コンピュータの加算命令列をシンボルplus
にも+
にも
結び付けられる(そのようなLispの方言もある)。
人間の世界でも、筆者をBob
と呼んだりRobert
と呼んだりでき、
それ以外の単語でもよい。
その一方で、シンボルには1つの関数定義しか結び付けられない。
さもなければ、どちらの定義を採用すべきかコンピュータが混乱する。
これを人間の世界に当てはめると、
Bob
という名前を持つ人は世界中で1人に限られることになる。
しかし、名前が参照する関数定義を変更するのは容易である
(See Install)。
Emacs Lispは巨大なので、関数が属するEmacsの構成部分がわかるように
シンボルを命名する慣習がある。
したがって、Texinfoを扱うすべての関数の名前はtexinfo-
で始まり、
メールを読むことに関連する関数の名前はrmail-
で始まる。
これまでの説明をもとに、リストの評価をLispインタープリタに命じると Lispインタープリタが何をするかを理解することができる。 まず、リストの直前にクオートがあるかどうかを調べる。 クオートがあれば、リストを返すだけである。 一方、クオートがなければ、インタープリタはリストの先頭要素を調べ、 それに関数定義があるかどうかを調べる。 関数定義があれば、インタープリタはその関数定義内の命令列を実行する。 さもなければ、インタープリタはエラーメッセージを出力する。
これがLispの動作であり、単純である。 すぐに説明するが、これに加えて複雑なことがらもあるが、以上が基本である。 当然、Lispプログラムを書くには、関数定義の書き方、名前への結び付け方、 および、これらを読者やコンピュータに混乱のないように行う方法を知る必要がある。
では、複雑なことがらの最初のことを説明しよう。 Lispインタープリタは、リストに加えて、 クオートもせず括弧で囲まれてもいないシンボルを評価できる。 この場合、Lispインタープリタは、 変数(variable)としてのシンボルの値を決定しようとする。 これについては変数に関する節で説明する (See Variables)。
複雑なことがらの2番目は、特殊な関数があり、 これらは普通の方式のように動作しないことである。 これらをスペシャルフォーム(special forms)と呼ぶ。 関数の定義などの特殊なことを行うものであり、それらの個数は多くはない。 以下のいくつかの章では、重要なスペシャルフォームのいくつかを紹介する。
3番目で最後の複雑なことがらはつぎのとおりである。 Lispインタープリタが探しあてた関数がスペシャルフォームでなく、 しかも、それがリストの一部である場合には、 Lispインタープリタはリストの内側にリストがあるかどうかを調べる。 内側にリストがあれば、Lispインタープリタは内側のリストを処理してから、 外側のリストを処理する。 内側のリストの内側にもリストが含まれている場合には、 それを処理してから内側のリストを処理する。 つねに、もっとも内側のリストを最初に処理する。 インタープリタはもっとも内側のリストの結果を得るためにそれを処理する。 その結果はそれを含む式で使われる。
そうでない場合には、インタープリタは左から右への順で式を1つずつ処理する。
インタープリタには別の側面もある。 Lispインタープリタは2種類のものを解釈できる。 本書で取り上げている人が読める形式のコードと、 特別な処理を施し人には読めない形式の バイトコンパイル(byte compiled)コードである。 バイトコンパイルしたコードは、人間向けのコードに比べて実行が速い。
byte-compile-file
のようなコンパイルコマンドを実行すれば、
人間向けのコードをバイトコンパイルコードに変換できる。
バイトコンパイルコードは、拡張子.el
ではなく拡張子.elc
で
終わるファイルに収容するのが一般的である。
ディレクトリemacs/lisp
には両方の種類のファイルがあるが、
読むのは拡張子.el
のファイルである。
実用上は、Emacsを調整したり拡張したりするのがほとんどであろうから、 バイトコンパイルする必要はなく、ここではこれ以上取り上げない。 バイトコンパイルについて詳しくはSee Byte Compilation。
Lispインタープリタが式を処理することを評価する(evaluation)と呼ぶ。 インタープリタが「式を評価する」という。 これまでにも、この用語を何度か使ってきた。 この用語は日常の言葉使いからきている。 つまり、Webster's New Collegiate Dictionaryによれば、 「価値や量を見定める、見積もること」である。
式を評価し終えると、Lispインタープリタは、関数定義で与えられた命令列を コンピュータが実行した結果を返す(return)のが一般的であるが、 関数の処理を諦めてエラーメッセージを生成する場合もある (インタープリタ自体を別の関数に渡したり、 「無限ループ」と呼ばれる無制限に同じことを繰り返すこともある。 これらの動作は一般的ではないので、ここでは無視することにする)。 ほとんどの場合、インタープリタは値を返す。
インタープリタは、値を返すと同時に、 カーソルを移動したりファイルをコピーしたりなどの別の動作も行う。 この種の別の動作は、副作用(side effect)と呼ばれる。 結果の表示などの人間が重要と考える動作は、 Lispインタープリタによっては「副作用」であることが多い。 この用語は奇妙に聞こえるかもしれないが、 副作用の使い方を学ぶのはかなり簡単である。
まとめると、Lispインタープリタは、 シンボリック式を評価するとほとんどの場合は値を返すが、副作用を伴うこともある。 あるいは、エラーを生成する。
内側にリストを含んだリストを評価するときには、 内側のリストを評価して得られた値を、 外側のリストを評価するときの情報として使う場合がある。 そのために、内側の式を最初に評価するのである。 それが返した値を外側の式で使用する。
このような評価の過程を、加算の例題で調べることにしよう。 つぎの式の直後にカーソルを置いてC-x C-eとタイプする。
(+ 2 (+ 3 3))
エコー領域には数8が表示される。
Lispインタープリタで行われることは、
まず内側の式(+ 3 3)
を評価することであり、これは値6を返す。
続いて、外側の式を(+ 2 6)
であるかのように評価し、これは値8を返す。
評価すべき外側の式はもうないので、
インタープリタはこの値をエコー領域に表示する。
さて、キー列C-x C-eで起動されるコマンドの名前を理解するのは容易である。
コマンドの名前はeval-last-sexp
である。
sexp
は「シンボリック式(symbolic expression)」の略、
eval
は「評価(evaluate)」の略である。
コマンドの意味は、「直前のシンボリック式を評価する」である。
式の直後の行の先頭や式の内側にカーソルを置いても式を評価できるかどうか 試してみよう。
つぎの式で試してみる。
(+ 2 (+ 3 3))
式の直後の空行の先頭にカーソルを置いてC-x C-eとタイプしても、
エコー領域には値8が表示される。
今度は、式の内側にカーソルを置いて試してみる。
最後の括弧のまえに(つまり、表示上は最後の括弧の上に)
カーソルを置いて評価すると、エコー領域には値6が表示される!
コマンドは式(+ 3 3)
を評価したからである。
今度は、数の直後にカーソルを置いてみる。
C-x C-eとタイプすると数そのものを得る。
Lispでは、数を評価するとその数そのものを得るのである。
これが数とシンボルの違いである。
+
のようなシンボルで始まるリストを評価すると、
+
に結び付けた関数定義の命令列を実行した結果の値を得る。
つぎの節で説明するように、シンボルそのものを評価すると別のことが起こる。
Lispでは、シンボルに関数定義を結び付けるように、 シンボルに値を結び付けることもできる。 これらの2つは異なるものである。 関数定義はコンピュータが遂行する命令列である。 一方で、値は、数や名前などの何かであり、変更できる (これが、そのようなシンボルが変数と呼ばれる理由である)。 シンボルの値としては、シンボル、数、リスト、文字列などの Lispの任意の式を取れる。 値を持つシンボルをしばしば変数(variable)と呼ぶ。
シンボルには関数定義と値の両方を同時に結び付けておくことができる。 これら2つは別々である。 これは、ケンブリッジという名称が、マサチューセッツの都市を表すと同時に、 「偉大なプログラミングセンター」のような名前の付加属性を持つことができるのに 似ている。
あるいは、シンボルは箪笥であると考えてほしい。 関数定義はある引き出しに入れてあり、値は別の引き出しに入れてあるのである。 関数定義を収めた引き出しの中身を変えることなく、 値を収めた引き出しの中身を変更でき、その逆もそうである。
値を持つシンボルの例として変数fill-column
を取り上げよう。
GNU Emacsの各バッファでは、このシンボルに72とか70の値が設定されるが、
それ以外の値の場合もある。
このシンボルの値を調べるには、それそのものを評価すればよい。
GNU EmacsのInfoで読んでいる場合には、
シンボルの直後にカーソルを置いてC-x C-eとタイプする。
fill-column
筆者の場合、C-x C-eとタイプするとEmacsはエコー領域に数72を表示する。
この値は、筆者が本書を執筆中にfill-column
に設定してある値である。
読者のInfoバッファでは異なるかもしれない。
変数の値として返された値は、関数の命令列を実行した結果返された値と
まったく同じように表示されることに注意してほしい。
Lispインタープリタの視点からは、どちらも返された値である。
値が求まってしまえば、それがどのような式から得られたかは関係ないのである。
シンボルにはどのような値でも結び付けることができる。
専門用語を使えば、変数には、72のような数、"such as this"
のような文字列、
(spruce pine oak)
のようなリストを束縛(bind)できる。
変数に関数定義を束縛することもできる。
シンボルに値を束縛する方法は何通りかある。 1つの方法は、See set & setq。
fill-column
の値を調べるために評価するときには、
単語fill-column
の周りには括弧がないことに注意してほしい。
これは、fill-column
を関数名としては使わないからである。
fill-column
がリストの先頭要素だとすると、
Lispインタープリタはこれに結び付けられた関数定義を探そうとする。
しかし、fill-column
には関数定義はない。
つぎを評価してみよう。
(fill-column)
すると、つぎのエラーメッセージを得る。 Symbol's function definition is void: fill-column
値が束縛されていないシンボルを評価すると、エラーメッセージを得る。
たとえば、2足す2の例を用いて調べてみよう。
つぎの式で、最初の数2のまえの+
の直後にカーソルを置いて
C-x C-eをタイプすると、
(+ 2 2)
つぎのエラーメッセージを得る。
Symbol's value as variable is void: +
これはまえに見たSymbol's function definition is void: this
とは
違うエラーメッセージである。
ここでは、シンボルには変数としての値がないのである。
まえの場合は、(this
という)シンボルには関数定義がなかったのである。
+
で試したことは、Lispインタープリタに+
を評価させて、
関数定義ではなく変数の値を探させたのである。
そうするために、式を閉じる括弧の直後にカーソルを置くかわりに、
シンボルの直後にカーソルを置いた。
そのため、Lispインタープリタは直前のS式、つまり、
+
そのものを評価したのである。
+
には、関数定義はあるが、値は束縛されていないので、
変数としてのシンボルの値は空(void)である旨のエラーメッセージが
報告されたのである。
どのように関数に情報が伝えられるかを見るために、 お馴染みの2足す2の例を使ってみよう。 Lispでは、つぎのように書く。
(+ 2 2)
この式を評価すると、エコー領域には数4が表示される。
Lispインタープリタが行ったことは、+
のあとに続く数の加算である。
+
が加算した数のことを、関数+
の引数(arguments)と呼ぶ。
これらの数は、関数に与えられた、つまり、渡された情報である。
「argument(引数)」という用語は数学での用法からきており、
2人のあいだの議論のことではない。
ここでは、+
という関数へ与えられた情報を意味する。
Lispでは、関数に対する引数は、その関数のあとに続くアトムやリストである。
これらのアトムやリストを評価して返された値が関数へ渡される。
関数が異なれば、必要な引数の個数も異なり、
引数をまったく必要としない関数もある。
2
関数に渡すデータの型は、関数が使用する情報の種類に応じて決まる。
+
は数を加算するので、
+
のような関数への引数は数値である必要がある。
別の関数では別の種類のデータの引数が必要である。
たとえば、関数concat
は複数の文字列を繋いで1つの文字列を生成する。
したがって、引数は文字列である。
文字列abc
とdef
を繋ぐ(concatinate)と、
1つの文字列abcdef
が作られる。
これはつぎの式を評価するとわかる。
(concat "abc" "def")
この式を評価して得られる値は"abcdef"
である。
substring
のような関数は、引数として文字列と数を取る。
この関数は文字列の一部分、つまり、第1引数の部分文字列を返す。
この関数は3つの引数を取る。
第1引数は文字列であり、第2引数と第3引数は数で、
部分文字列の開始位置と終了位置を示す。
これらの数は、文字列の先頭からの(空白や句読点を含む)文字の個数である。
たとえば、つぎを評価すると、
(substring "The quick brown fox jumped." 16 19)
エコー領域には"fox"
と表示される。
引数は、1つの文字列と2つの数である。
substring
に渡された文字列は、
空白で区切られた複数の単語ではあるが1つのアトムである。
Lispは、2つの二重引用符のあいだにあるものを、
たとえ空白があいだにあっても文字列として扱う。
関数substring
は、他の方法では不可分なアトムから一部分を切り出すので、
「アトム粉砕機」の一種と考えてもよい。
しかし、substring
は、文字列である引数から部分文字列を切り出すことが
できるだけであり、数やシンボルなどのそれ以外の種類のアトムは扱えない。
引数は、評価したときに値を返すシンボルでもよい。
たとえば、シンボルfill-column
そのものを評価すると数を返す。
この数は加算に使える。
つぎの式のうしろにカーソルを置いてC-x C-eとタイプする。
(+ 2 fill-column)
fill-column
を単独で評価した値より2だけ大きな値が得られる。
筆者の場合には、fill-column
の値は72なので、結果は74である。
このように、評価すると値を返すシンボルを引数に使えるのである。
さらに、評価すると値を返すリストを引数に使うこともできる。
たとえば、つぎの式では、関数concat
への引数は、
文字列"The "
と" red foxes."
、
さらに、リスト(+ 2 fill-column)
である。
(concat "The " (+ 2 fill-column) " red foxes.")
この式を評価すると、エコー領域には"The 74 red foxes."
と表示される
(最終結果の文字列に空白が含まれるように、
単語The
の直後と単語red
の直前には空白が必要である)。
concat
、+
、*
などのある種の関数は、任意個数の引数を取る
(*
は乗算のシンボルである)。
これは、以下の式のおのおのを通常の方法で評価するとわかる。
=>
のあとのテキストがエコー領域に表示され、
=>
は「の評価結果は」と読めばよい。
まず、関数に引数がない場合にはつぎのとおりである。
(+) => 0 (*) => 1
つぎは、関数に引数が1つの場合である。
(+ 3) => 3 (* 3) => 3
今度は、関数に引数が3つある場合である。
(+ 3 4 5) => 12 (* 3 4 5) => 60
誤った型の引数を関数へ渡すと、Lispインタープリタはエラーメッセージを生成する。
たとえば、関数+
は引数として数を仮定する。
数のかわりにクオートしたシンボルhello
を与えてみよう。
つぎの式の直後にカーソルを置いてC-x C-eとタイプする。
(+ 2 'hello)
そうすると、エラーメッセージを得る。
何が起こったかというと、+
は数2に'hello
が返す値を
加算しようとしたのだが、'hello
が返した値はシンボルhello
であり、
数ではない。
加算できるのは数のみである。
したがって、+
は加算を実行できなかったのである。
普通、エラーメッセージは有用なようになっているので、 読み方がわかれば意味がわかる。 エラーメッセージはつぎのとおりである。
Wrong type argument: integer-or-marker-p, hello
エラーメッセージの最初の部分は簡単で、
Wrong type argument
、つまり、引数の型がまちがっているである。
つぎは、不思議な専門用語でinteger-or-marker-p
である。
これは、+
が仮定する引数の種類を伝えようとしているのである。
シンボルinteger-or-marker-p
の意味は、Lispインタープリタが
渡された情報(引数の値)が整数(つまり数)かマーカー(バッファ内の位置を表す
特殊なオブジェクト)かどうかを調べようとしているということである。
つまり、加算すべき整数が+
に与えられたかどうかを検査したのである。
このとき、引数が、Emacs Lispに特有の機能であるマーカーかどうかも検査する
(Emacsでは、バッファ内の位置をマーカーで記録する。
コマンドC-@やC-<SPC>でマークを設定すると、
その位置はマーカーとして保存される。
マークは数として扱うことができ、
バッファの先頭からのその位置までの文字の個数である)。
Emacs Lispでは、+
でマーカー位置の数値を数として加算できる。
integer-or-marker-p
のp
は、
Lispプログラミングの早い時期に始まった慣習である。
p
は「述語(predicate)」の略である。
初期のLisp研究者が用いた専門用語では、
述語とはある性質が真か偽か調べる関数を意味する。
したがって、p
があることで、integer-or-marker-p
は、
与えられた引数が整数であるかマーカーであるかの真偽を調べる関数の名前である
ことがわかる。
p
で終わるそのほかのLispシンボルには、
引数の値が0であるかを調べる関数zerop
、
引数がリストであるかを調べる関数listp
がある。
エラーメッセージの最後の部分はシンボルhello
である。
これは+
に渡された引数の値である。
正しい型のオブジェクトが加算に渡されれば、
渡された値はhello
のようなシンボルではなく37のような数であるはずである。
そうであったならば、エラーメッセージが表示されることはなかったのである。
message
+
のように、関数message
は任意個数の引数を取る。
この関数はユーザーにメッセージを表示するために使うので、
ここで説明しておくのがよいであろう。
メッセージはエコー領域に表示される。 たとえば、つぎのリストを評価すると、エコー領域にメッセージが表示される。
(message "This message appears in the echo area!")
二重引用符のあいだの文字列は1つの引数であり、一塊で表示される
(ここでの例では、エコー領域に表示されるメッセージは二重引用符で囲まれる。
これは、関数message
が返した値を見ているからである。
プログラム内でのmessage
のほとんどの使い方では、
副作用としてエコー領域にテキストが表示されるので、
表示には引用符は含まれない。
このような例について詳しくは、
See multiply-by-seven in detail)。
さて、文字列の中に%s
がある場合、
関数message
は%s
をそのとおりに表示することはなく、
文字列のあとに続く引数を調べる。
第2引数を評価し、その値を文字列の%s
の位置に表示する。
つぎの式の直後にカーソルを置いてC-x C-eとタイプしてみる。
(message "The name of this buffer is: %s." (buffer-name))
Infoでは、エコー領域に"The name of this buffer is: *info*."
と
表示されるはずである。
関数buffer-name
はバッファ名を文字列として返し、
それを関数message
が%s
の位置に挿入する。
値を10進数として表示するには%s
のかわりに%d
を使う。
たとえば、fill-column
の値を含んだメッセージをエコー領域に表示するには
つぎの式を評価する。
(message "The value of fill-column is %d." fill-column)
筆者のシステムでは、このリストを評価するとエコー領域に
"The value of fill-column is 72."
と表示される。
文字列の中に複数の%s
があれば、
文字列のあとに続く最初の引数の値が最初の%s
の位置に表示され、
2番目の引数の値が2番目の%s
の位置に表示されると続く。
たとえば、つぎの式を評価すると
(message "There are %d %s in the office!" (- fill-column 14) "pink elephants")
エコー領域に少々妙なメッセージが表示される。
筆者のシステムでは"There are 58 pink elephants in the office!"
となる。
式(- fill-column 14)
が評価され、
その結果の数が%d
の位置に挿入される。
二重引用符の中の文字列"pink elephants"
は
1つの引数として扱われて%s
の位置に挿入される
(つまり、数と同様に、二重引用符で囲まれた文字列を評価するとそれ自身となる)。
最後は少々複雑な例で、数の計算を示すとともに、
%s
と置き換わるテキストを生成する式を式の内側で使う方法を示す。
(message "He saw %d %s" (- fill-column 34) (concat "red " (substring "The quick brown foxes jumped." 16 21) " leaping."))
この例では、message
には、文字列"He saw %d %s"
、
式(- fill-column 32)
、関数concat
で始まる式の3つの引数がある。
(- fill-column 32)
を評価した結果は%d
の位置に挿入され、
concat
で始まる式の値は%s
の位置に挿入される。
筆者の場合、この式を評価するとエコー領域に
"He saw 38 red foxes leaping."
と表示される。
変数に値を与える方法はいくつかある。
1つの方法は関数set
か関数setq
を使うことである。
別の方法はlet
(see let)を使うことである
(この過程を専門用語では変数を値に束縛(bind)するという)。
つぎの節ではset
とsetq
の動作を説明するとともに、
引数がどのように渡されるかも説明する。
setq
to count.
set
の使い方シンボルflowers
の値としてリスト'(rose violet daisy buttercup)
を
設定するには、つぎの式の直後にカーソルを移動してC-x C-eとタイプして
式を評価する。
(set 'flowers '(rose violet daisy buttercup))
エコー領域には、リスト(rose violet daisy buttercup)
が表示される。
これは、関数set
が返したものである。
副作用として、シンボルflowers
にリストが束縛される。
つまり、シンボルflowers
は変数とみなされ、
その値としてリストが与えられるのである
(値を設定するこの過程は、Lispインタープリタにとっては副作用であるが、
人間にとっては興味のある主要な効果である。
各Lisp関数はエラーがなければ値を返す必要があるが、
関数の目的は副作用だけの場合もある)。
set
式を評価したあとは、シンボルflowers
を評価することができ、
設定した値が返される。
つぎのシンボルの直後にカーソルを置いてC-x C-eとタイプする。
flowers
flowers
を評価すると、
エコー領域にリスト(rose violet daisy buttercup)
が表示される。
'flowers
、つまり、直前にクオートを置いた変数を評価すると、
エコー領域にはシンボルflowers
そのものが表示される。
つぎの例を試してみよう。
'flowers
set
を使う場合、いずれの引数も評価してほしくない場合には、
両方の引数をクオートする必要があることに注意してほしい。
上の例では、変数flowers
もリスト(rose violet daisy buttercup)
も
評価したくないので、両者をクオートした
(set
の第1引数をクオートせずに使うと、まず最初に第1引数が評価される。
上の例でこれを行うと、flowers
には値がないので、
Symbol's value as variable is void
というエラーメッセージを得る。
一方、flowers
を評価して値が返される場合には、
set
はその返された値に値を設定しようとする。
このように関数を動作させたい場面もあるが、そのような場面は少ない)。
setq
の使い方実用上、set
の第1引数をほとんどつねにクオートするはずである。
set
で第1引数をクオートする組み合わせは多用されるので、
スペシャルフォームsetq
が用意してある。
このスペシャルフォームはset
とほとんど同じであるが、
第1引数を自動的にクオートするので、引用符をタイプする必要はない。
さらに、便利なように、setq
では、複数の異なる変数に異なる値を
1つの式で設定することもできる。
setq
を用いて、変数carnivores
の値を
リスト'(lion tiger leopard)
とするには、つぎの式を使う。
(setq carnivores '(lion tiger leopard))
これはset
を用いた場合とほぼ同じであるが、
setq
では第1引数を自動的にクオートするのが異なる
(setq
のq
は、クオート(quote
)を意味する)。
set
を用いる場合は、つぎのようにする。
(set 'carnivores '(lion tiger leopard))
さらに、setq
は、複数の異なる変数に異なる値を代入するためにも使える。
第1引数には第2引数の値を束縛し、第3引数には第4引数の値を束縛し、……と続く。
たとえば、シンボルtrees
に樹木名のリストを、
シンボルherbivores
にハーブの名前のリストを代入するにはつぎのようにする。
(setq trees '(pine fir oak maple) herbivores '(gazelle antelope zebra))
(式を1行に書いてもよいが、ページの幅に収まらない。 人間には、適切にフォーマットしたリストは読みやすいものである。)
「代入」という用語を用いたが、set
やsetq
の動作を考える
別の方法もある。
つまり、set
やsetq
は、シンボルがリストを指す(point)
ようにするのである。
この考え方は非常によく使われ、うしろの章では、名前の一部に「pointer」がある
シンボルを見ることになる。
この名称は、シンボルには特にリストなどの値が結び付けらている、
あるいは、シンボルがリストを「指す」ように設定されていることに由来する。
ここではカウンタでsetq
を使う方法を示そう。
プログラムのある部分を何回繰り返したかを数え上げるために使える。
まず、変数に0を設定する。
そして、プログラムを繰り返すたびに1を加える。
そのためには、カウンタの役割を果たす変数と2つの式、つまり、
カウンタ変数を0に初期化するsetq
を用いた式と、
評価するたびにカウンタを増加するsetq
を用いた式が必要である。
(setq counter 0) ; 初期化式 (setq counter (+ counter 1)) ; 増加式 counter ; カウンタ
(;
以降のテキストは注釈である。
See Change a defun。)
これらの最初の式、つまり、初期化式(setq counter 0)
を評価してから
3番目の式counter
を評価すると、エコー領域には数0
が表示される。
続いて、2番目の式、増加式(setq counter (+ counter 1))
を評価すると、
カウンタの値は1になる。
そのため、ふたたびcounter
を評価すると
エコー領域には数1
が表示される。
2番目の式を評価するたびに、カウンタの値は増加する。
増加式(setq counter (+ counter 1))
を評価するとき、
Lispインタープリタは、もっとも内側のリスト、つまり、加算を最初に評価する。
このリストを評価するには、変数counter
と数1
を評価する必要がある。
変数counter
を評価するとその現在値が得られる。
この値と数1
が+
に渡され加算される。
この合計値がもっとも内側のリストの値として返され、
さらに、変数counter
にこの新しい値を設定するsetq
へ渡される。
したがって、変数counter
の値が変更されるのである。
Lispを学ぶことは、登り始めがもっとも険しい小山を登るようなものである。 読者はもっとも困難な部分を登り終えたのであり、 あとは、先へ進むほど楽になる。
本章をまとめるとつぎのようになる。
forward-paragraph
のような複数個の文字のシンボル、
+
のような1文字のシンボル、二重引用符で囲った文字列、あるいは、数である。
'
は、Lispインタープリタに対して続く式を字面のとおりに
返すことを指示し、引用符がない場合のようには評価しない。
簡単な演習問題をあげておく。
Emacs Lispにおける関数定義の書き方を学ぶまえに、これまでに説明してきた さまざまな式の評価に時間を割いてみるのも有益であろう。 リストの先頭(あるいは唯一)の要素が関数であるようなリストである。 バッファに関する関数は、単純でしかも興味深いので、これらから始めよう。 本節では、それらのいくつかを評価してみる。 他の節では、バッファに関連した数個の別の関数のコードを調べて、 それらがどのように書かれているかを見てみる。
Emacs Lispに、カーソルの移動や画面上のスクロールなどの 編集コマンドを与えるたびに、先頭要素が関数である式を評価している。 Emacsはこのようにして動いている。
キーをタイプすると、Lispインタープリタに式を評価させることになり、
そのようにして操作しているのである。
テキストをタイプした場合でさえも、Emacs Lispの関数を評価しており、
タイプした文字を単に挿入する関数self-insert-command
を使った
関数を評価しているのである。
キーをタイプすることで評価される関数は、
対話的(interactive)関数とかコマンド(commands)と呼ばれる。
関数を対話的にする方法については、関数定義の書き方の章で例を示す。
See Interactive。
キーボードコマンドをタイプする以外にも、 式を評価する方法についてはすでに説明した。 すなわち、リストの直後にカーソルを置いてC-x C-eとタイプするのである。 本節の残りでは、この方法を用いる。 これ以外にも式を評価する方法があり、必要に応じて他の節で説明する。
これからの数節で説明する関数は、評価を練習すること以外にも、 それ自体、重要なものである。 これらの関数を学ぶことで、バッファとファイルの違い、 バッファを切り替える方法、バッファ内での位置を調べる方法が明らかになる。
2つの関数、buffer-name
とbuffer-file-name
が、
ファイルとバッファの違いを示してくれる。
式(buffer-name)
を評価すると、エコー領域にバッファ名が表示される。
(buffer-file-name)
を評価すると、
バッファが参照するファイル名がエコー領域に表示される。
通常、(buffer-name)
が返す名前は、
そのバッファが参照するファイルの名前と同じであり、
(buffer-file-name)
が返す名前はファイルの完全パス名である。
ファイルとバッファは異なる2つの実体である。 ファイルは(削除しない限り)コンピュータに恒久的に記録された情報である。 一方、バッファはEmacsの内部にある情報であり、 (バッファを削除するか)編集作業を終了すると消えてしまう。 通常、バッファにはファイルからコピーした情報が収められている。 これを、バッファがファイルを訪れる(visiting)という。 このコピーを処理したり修正したりしているのである。 バッファを変更しても保存しない限り、ファイルは変更されない。 バッファを保存すると、バッファはファイルにコピーされ、 その結果、恒久的に保存されるのである。
GNU EmacsのInfoを使って読んでいる場合には、 つぎのそれぞれの式の直後にカーソルを置いてC-x C-eとタイプすれば、 それぞれの式を評価できる。
(buffer-name) (buffer-file-name)
筆者がこれを行うと、(buffer-name)
を評価して返される値は
"introduction.texinfo"
であり、
(buffer-file-name)
を評価して返される値は
"/gnu/work/intro/introduction.texinfo"
である。
前者はバッファ名であり、後者はファイル名である
(各式には括弧があるので、Lispインタープリタはbuffer-name
と
buffer-file-name
を関数として扱う。
括弧がないと、インタープリタは変数としてシンボルを評価しようとする。
See Variables)。
ファイルとバッファの違いにもかかわらず、 バッファを意味してファイルといったり、その逆のいい方をする場合が多い。 もちろん、ほとんどの人は、「すぐにファイルに保存するバッファを編集している」 というよりは、「ファイルを編集している」という。 その人が何を意図しているかは、ほとんどの場合、文脈から明らかである。 しかし、コンピュータプログラムに関していえば、 コンピュータは人間のように賢くはないので、 つねに違いを心にとめておく必要がある。
ところで、用語「バッファ(buffer)」は、衝突力を弱めるクッションを意味する 語に由来する。 初期のコンピュータでは、ファイルとコンピュータの中央処理装置のあいだの 相互作用のクッションがバッファであった。 ファイルを格納するドラムやテープと中央処理装置とは、互いに大きく異なる 装置であり、それぞれ固有の動作速度で動いていた。 バッファにより、これらが効率よく協調動作することが可能であった。 しだいに、バッファは、中間の一時的な保管場所から、 実際の処理を行う場所へと進展していった。 これは、小さな港が大都市に発展したり、貨物を船に積み込むまえの一時的な保管倉庫 がその重要性のためにビジネスや文化の中心に進展したことに似ている。
すべてのバッファがファイルに関連付けられるわけではない。
たとえば、ファイル名をいっさい指定せずにコマンドemacs
だけをタイプして
Emacsを開始した場合には、画面にはバッファ*scratch*
が表示されて
Emacsが動き出す。
このバッファはいかなるファイルも訪問していない。
同様に、バッファ*Help*
にはいかなるファイルも関連付けられていない。
バッファ*scratch*
に切り替えてから、(buffer-name)
と入力して
その直後にカーソルを置いてC-x C-eとタイプしてこの式を評価すると、
名前"*scratch*"
が返されてエコー領域に表示される。
"*scratch*"
がバッファの名前である。
しかし、バッファ*scratch*
で(buffer-file-name)
とタイプして
これを評価すると、エコー領域にはnil
と表示される。
nil
の語源はラテン語の「無(nothing)」を意味する単語であり、
この場合、バッファ*scratch*
にはいかなるファイルも関連付けられていない
ことを意味する
(Lispでは、nil
は「偽(false)」をも意味し、
空リスト()
の同義語でもある)。
バッファ*scratch*
を使っているときに、
式の評価結果をエコー領域ではなくバッファ*scratch*
そのものに
表示したい場合には、C-x C-eのかわりにC-u C-x C-eとタイプする。
これにより、式の直後に返された値が表示される。
バッファはつぎのようになる。
(buffer-name)"*scratch*"
Infoは読み出し専用なのでバッファの内容を変更できないため、 Infoではこの方法を使えない。 しかし、編集可能なバッファならば、この方法を使える。 コードや(本書のような)文書を書くときには、この機能はとても便利である。
関数buffer-name
は、バッファの名前を返す。
バッファそのものを取得するには、
別の関数current-buffer
が必要である。
この関数をコードで使うと、バッファそのものを取得することになる。
名前とその名前が表すオブジェクトや実体とは互いに異なるものである。
読者自身は読者の名前ではない。
読者は、その名前で他人が参照する「人」である。
読者がGeorgeと話したいと頼んだときに、G
、e
、o
、r
、
g
、e
と文字が書かれたカードを渡されたら、驚くであろうが、
満足はしないであろう。
名前と話をしたいのではなくて、その名前で参照される人と話をしたいのである。
バッファも同様である。
一時的バッファの名前は*scratch*
であるが、
名前そのものがバッファではない。
バッファそのものを得るにはcurrent-buffer
のような関数を使う必要がある。
しかし、多少込み入った事情もある。
ここで試すように、式でcurrent-buffer
を評価すると
バッファの内容ではなくバッファの表示形式が表示される。
Emacsがこのように動作する理由は2つある。
バッファには数千もの行が含まれることもあるので、
これを簡便に表示するには長すぎる。
また、バッファの内容は同じであっても、名前が異なるバッファもありえるので、
それらを区別できる必要がある。
例を示そう。
(current-buffer)
いつものようにこの式を評価すると、
エコー領域には#<buffer *info*>
と表示される。
バッファ名ではなく、バッファそのものを表す特殊な表示形式である。
数やシンボルはプログラムに入力できるが、
バッファの表示形式を入力することはできない。
バッファそのものを得る唯一の方法は、
current-buffer
のような関数を使うことである。
関連する関数としてother-buffer
がある。
これは、現在使用しているバッファの直前に選択していたバッファを返す。
たとえば、バッファ*scratch*
から現在のバッファに切り替えた場合には、
other-buffer
はバッファ*scratch*
を返す。
つぎの式を評価してみよう。
(other-buffer)
エコー領域には、#<buffer *scratch*>
、あるいは、
今のバッファに切り替えるまえのバッファの表示形式が表示される。
関数other-buffer
の目的は、バッファそのものを引数に必要とする
関数にバッファを与えることである。
別のバッファに切り替えるためother-buffer
とswitch-to-buffer
とを
使ってみよう。
まず、関数switch-to-buffer
の概要を説明しておこう。
(buffer-name)
を評価するためにInfoからバッファ*scratch*
へ
切り替えるときには、C-x bとタイプし、ミニバッファに表示された
切り替え先バッファ名のプロンプトに*scratch*
と入力したであろう。
C-x bとタイプすると、LispインタープリタはEmacs Lispの対話的関数
switch-to-buffer
を評価する。
すでに説明したように、Emacsはこのように動作する。
キー列が異なれば、異なる関数を呼び出す、つまり、実行するのである。
たとえば、C-fとタイプするとforward-char
を呼び出し、
M-eとタイプするとforward-sentence
を呼び出すなどである。
switch-to-buffer
を書いた式に切り替え先のバッファを指定すれば、
C-x bと同じようにバッファを切り替えられる。
たとえば、つぎのLispの式である。
(switch-to-buffer (other-buffer))
リストの先頭要素はシンボルswitch-to-buffer
であるので、
Lispインタープリタはこれを関数として扱い、それに結び付けられている
命令列を実行する。
しかし、そのまえに、インタープリタはother-buffer
が括弧の内側にあることを
検出して、まずそのシンボルを処理する。
other-buffer
はこのリストの先頭(かつ唯一の)要素なので、
Lispインタープリタはこの関数を呼び出す。
これは別のバッファを返す。
続いて、インタープリタは、この別のバッファを引数として
switch-to-buffer
に渡して実行し、そのバッファへ切り替える。
Infoで読んでいる場合には、この式を評価してみてほしい
(Infoのバッファに戻るにはC-x b <RET>とタイプする)。
本書の以降の節のプログラム例では、関数switch-to-buffer
よりも
関数set-buffer
を多用する。
これは、コンピュータプログラムと人間との違いによるものである。
人間には目があるので、端末画面で作業中のバッファを見ることを期待する。
これは当然のことであり、これ以上説明する必要はなかろう。
一方、プログラムに目はない。
コンピュータプログラムがバッファを処理するときに、
端末画面でバッファが見えている必要はない。
switch-to-buffer
は人間向けに考えられたものであり、
異なる2つのことを行う。
Emacsの注意をバッファに向けることと、
ウィンドウに表示するバッファをそのバッファに切り替えることである。
一方、set-buffer
は1つのことだけを行い、
コンピュータプログラムの注意をそのバッファに向けるだけである。
画面上のバッファは変更しない
(もちろん、コマンドが終了するまでは、何も起こらない)。
ここでは、新たな専門用語呼び出し(call)も使った。 リストの先頭のシンボルが関数であるようなリストを評価すると、 その関数を呼び出すのである。 この用語は、鉛管工を「呼ぶ」とパイプの洩れを修理してくれるのと同じように、 関数は、「呼び出す」と何かを行ってくれる実体であるという考え方からきている。
最後に、buffer-size
、point
、point-min
、point-max
などの比較的単純な関数を見てみよう。
これらにより、バッファのサイズやバッファ内のポイントの位置に関する情報を
得ることができる。
関数buffer-size
は、カレントバッファのサイズを返す。
つまり、バッファ内の文字の個数を返す。
(buffer-size)
いつものように、この式の直後にカーソルを置いてC-x C-eとタイプすれば、 この式を評価できる。
Emacsでは、カーソルの現在位置をポイント(point)と呼ぶ。
式(point)
は、バッファの先頭からポイントまでの文字の個数として、
カーソルの位置を返す。
いつものようにつぎの式を評価すると、 バッファ内でのポイントまでの文字数を調べることができる。
(point)
筆者の場合、この値は65724であった。
本書の例では、関数point
を多用している。
ポイントの値は、当然であるが、バッファ内での位置に依存する。 ここでつぎの式を評価すると、より大きな数になる。
(point)
筆者の場合、ポイントの値は66043となり、 2つの式のあいだには(空白を含めて)319文字あることがわかる。
関数point-min
は、point
とほぼ同じであるが、
カレントバッファにおいて取ることが可能なポイントの最小値を返す。
ナロイング(narrowing)していなければ、これは数1である
(ナロイング(偏狭化)とは、プログラムなどで操作するバッファの範囲を
制限する機構である。
See Narrowing & Widening)。
同じように、関数point-max
は、
カレントバッファにおいて取ることが可能なポイントの最大値を返す。
適当なファイルを読み込み、その中ほどに移動する。 バッファ名、ファイル名、長さ、ファイル内での位置のそれぞれを調べてみよ。
リストを評価するとき、Lispインタープリタは、 リストの先頭のシンボルに関数定義が結び付けられているかどうかを調べる。 いいかえれば、シンボルが関数定義を指すかどうかを調べる。 そうならば、コンピュータは定義内の命令列を実行する。 関数定義を持つシンボルを、単に関数と呼ぶ (しかし、正確には、定義が関数であり、シンボルはそれを指すだけである)。
defun
special form.
interactive
.
すべての関数は別の関数を用いて定義されているが、 プログラミング言語Cで書かれた少数の 基本操作(primitive)関数はそうではない。 関数の定義を書くときには、他の関数を構成部品として用いてEmacs Lispで書く。 そのとき使用する関数は、 それ自身Emacs Lispで(読者自身が)書いたものであったり、 Cで書かれた基本操作関数である。 基本操作関数は、Emacs Lispで書いたものとまったく同じように使え、 そのように動作する。 これらをCで書いてあるのは、Cが動く十分な能力を持ったコンピュータならば 容易にGNU Emacsを動作できるようにするためである。
強調しておくが、Emacs Lispでコードを書くとき、 Cで書いた関数の使い方とEmacs Lispで書いた関数の使い方とは区別しない。 両者の違いは無関係なのである。 わざわざ言及したのは、興味深いと考えたからである。 既存の関数がEmacs Lispで書いてあるのか、Cで書いてあるのかは、 特に調べない限りわからない。
defun
Lispでは、mark-whole-buffer
のようなシンボルには、
関数として呼ばれたときにコンピュータが実行するコードが結び付けられている。
このコードを関数定義(function definition)と呼び、
シンボルdefun
(define function(関数を定義する)の略)で始まる
Lispの式を評価することで作成する。
defun
は、その引数を通常のようには評価しないので、
スペシャルフォーム(special form)と呼ばれる。
以下の節では、mark-whole-buffer
のようなEmacsのソースコードの
関数定義を調べる。
本節では、関数定義がどのようなものかを理解してもらうために、
簡単な関数定義を説明する。
例を簡単にするために、算術演算を使った関数定義を取り上げる。
算術演算を使った例が嫌いな人もいるであろうが、落胆しないでほしい。
残りの節で説明するコードには、算術演算や数学はほとんどない。
そのほとんどは、テキストを扱う。
関数定義は、単語defun
に続く最大で5つの部分から成る。
()
を指定する。
関数定義の5つの部分を、つぎのような雛型にまとめて考えるとわかりやすい。
(defun 関数名 (引数...) "省略可能な関数の説明文..." (interactive 引数に関する情報) ; 省略可能 本体...)
例として、引数を7倍する関数のコードを示す (この例は、対話的関数ではない。 これについては、See Interactive)。
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number))
この定義は、括弧とシンボルdefun
で始まり、関数名が続く。
関数名のあとには、関数に渡される引数のリストが続く。
このリストを、引数リスト(argument list)と呼ぶ。
この例では、リストには1つの要素、シンボルnumber
のみがある。
関数が使われると、関数への引数として使われた値がこのシンボルに束縛される。
引数の名前としては、単語number
のかわりに別の名前を指定してもよい。
たとえば、単語multiplicand
でもよい。
引数の値の種類がわかるように単語number
を選んだ。
同様に、関数の実行において引数の役割を表すmultiplicand
(被乗数)を
選んでもよかった。
引数をfoogle
とも書けるが、これでは何を意味するか不明なのでよくない。
名前の選択はプログラマの責任であり、
関数の意味を明らかにするように選ぶべきである。
引数リストのシンボルにはどんな名前を選んでもよく、
他の関数で使っているシンボルの名前でもよい。
引数リストに使用した名前は、その定義において私的である。
つまり、その定義において名前で参照した実体は、
その関数定義の外部で同じ名前で参照する実体とは別である。
たとえば、家族のあいだでは読者の愛称は「ショーティ」だとしよう。
家族の誰かが「ショーティ」といった場合には、読者のことである。
しかし、別の家族が「ショーティ」といった場合には、別の誰かのことである。
引数リスト中の名前は関数定義に私的なので、関数本体の内側でそのようなシンボルの
値を変更しても、関数の外部の値には影響しない。
let
式でも同様な効果が得られる
(See let)。
引数リストには、関数の説明文である文字列が続く。
C-h fに続けて関数名をタイプしたときに表示されるのは、
この文字列である。
apropos
などのある種のコマンドでは複数行の説明文のうち最初の1行のみを
表示するので、関数の説明文を書く場合には、最初の1行を1つの文にすべきである。
また、C-h f(describe-function
)で表示した場合に、
表示が変にならないように、説明文の2行目以降を字下げしないこと。
説明文は省略できるが、あると有益なので、
読者が書くほとんどの関数には指定するべきである。
上の例の3行目は関数定義の本体である
(当然、ほとんどの関数の定義は、この例よりも長いはずである)。
ここでは、本体はリスト(* 7 number)
であり、
numberの値を7倍する
(Emacs Lispでは、+
が加算であるように、*
は乗算である)。
関数multiply-by-seven
を使うときには、
引数number
は読者が指定した実際の数に評価される。
multiply-by-seven
の使い方を示すが、まだ、評価しないでほしい。
(multiply-by-seven 3)
関数を実際に使用すると、次節の関数定義に指定したシンボルnumber
には、
値3が与えられる、つまり、「束縛」される。
関数定義ではnumber
は括弧の内側にあるが、
関数multiply-by-seven
に渡される引数は括弧の内側にはないことに
注意してほしい。
関数定義において引数を括弧で囲むのは、コンピュータが
引数リストの終わりと関数定義の残りの部分を区別できるようにするためである。
さて、この例を評価すると、エラーメッセージを得る (実際に試してみるとよい)。 これは、関数定義を書いたけれども、その定義をコンピュータに 与えていないからである。 つまり、関数定義をEmacsにインストール(あるいは、ロード)していないからである。 関数のインストールとは、Lispインタープリタに関数の定義を教える操作である。 次節では、インストールについて説明する。
EmacsのInfoで読んでいる場合には、multiply-by-seven
の関数定義を
評価してから(multiply-by-seven 3)
を評価すれば、
関数multiply-by-seven
を試すことができる。
関数定義をもう一度つぎにあげておく。
関数定義の最後の括弧の直後にカーソルを置いてC-x C-eとタイプする。
すると、エコー領域にmultiply-by-seven
と表示される
(関数定義を評価すると、その値として定義された関数の名前が返される)。
同時に、この操作で関数定義がインストールされるのである。
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number))
このdefun
を評価すると、Emacsにmultiply-by-seven
をインストール
したことになる。
これで、forward-word
や編集関数などと同じく、
この関数もEmacsの一部である
(Emacsを終了するまで、multiply-by-seven
はインストールされたままである。
Emacs起動時に自動的にコードをロードするには
Permanent Installationを参照)。
つぎの例を評価すれば、multiply-by-seven
をインストールした効果がわかる。
つぎの式の直後にカーソルを置いてC-x C-eとタイプする。
エコー領域に数21が表示されるはずである。
(multiply-by-seven 3)
必要ならば、C-h f(describe-function
)に続けて
関数名 multiply-by-seven
をタイプすれば、
関数の説明文を読むことができる。
これを行うと、つぎのような内容のウィンドウ*Help*
が画面に現れる。
multiply-by-seven: Multiply NUMBER by seven.
(画面を単一のウィンドウに戻すには、C-x 1とタイプする。)
multiply-by-seven
のコードを変更するには、書き変えればよい。
旧版のかわりに新版をインストールするには、関数定義を再度評価する。
Emacsではこのようにコードを修正すればよく、非常に簡単である。
例として、7を掛けるかわりに、数そのものを7回足すように
関数multiply-by-seven
を変更する。
同じ結果を得るが、その方法が異なる。
同時に、コードに注釈を加えよう。
注釈とは、Lispインタープリタは無視するテキストであるが、
人には有用であり意味を明らかにする。
この例では、「第2版」が注釈である。
(defun multiply-by-seven (number) ; 第2版 "Multiply NUMBER by seven." (+ number number number number number number number))
セミコロン;
に続けて注釈を書く。
Lispでは、行の中でセミコロンに続くものはすべて注釈である。
注釈は行末で終わる。
2行以上にわたる注釈は、各行をセミコロンで始める。
注釈に関してより詳しくは、 Commentsや See Beginning a .emacs File。
この版の関数multiply-by-seven
をインストールするには、
最初の版を評価したのと同じように評価すればよい。
すなわち、最後の括弧の直後にカーソルを置いてC-x C-eとタイプする。
まとめると、Emacs Lispでコードを書くには、 関数を書いてインストールしてテストし、 必要に応じて、修正や機能強化して再インストールする。
関数を対話的にするには、
関数の説明文のあとにスペシャルフォームinteractive
で始まるリストを置く。
こうすれば、M-xに続けて関数名をタイプするか、
関数に束縛したキー列をタイプすれば対話的関数を起動できる。
たとえば、next-line
を起動するにはC-nとタイプし、
mark-whole-buffer
を起動するにはC-x hとタイプする。
対話的関数を対話的に呼び出した場合には、 関数が返した値は自動的にはエコー領域に表示されない。 これは、対話的関数を呼び出すのは、単語単位や行単位の移動などの副作用の ためであり、返された値は必要ないからである。 キーをタイプするたびに返された値をエコー領域に表示すると、とても煩わしい。
multiply-by-seven
の対話的な版を作って、
スペシャルフォームinteractive
の使い方と
エコー領域に値を表示する1つの方法を示そう。
コードはつぎのとおりである。
(defun multiply-by-seven (number) ; 対話的版 "Multiply NUMBER by seven." (interactive "p") (message "The result is %d" (* 7 number)))
このコードの直後にカーソルを置いてC-x C-eとタイプして、
このコードをインストールする。
エコー領域には関数名が表示されるはずである。
そうすれば、C-u、数、M-x multiply-by-sevenとタイプして
<RET>を押せば、このコードを使うことができる。
エコー領域には、
The result is ...
に続けて乗算結果が表示されるはずである。
より一般的には、このような関数を起動する方法は2つある。
上のキー列の例は、どちらもポイントを文単位に3つ進める
(multiply-by-seven
にはキーがバインドされていないので、
キーバインドを使う例としては使えない)。
(コマンドをキーにバインドする方法については See Keybindings。)
前置引数を対話的関数に渡すには、 M-3 M-eのようにキー<META>に続けて数をタイプするか、 C-u 3 M-eのようにC-uに続けて数をタイプする (数をタイプせずにC-uだけをタイプすると、デフォルトは4)。
multiply-by-seven
スペシャルフォームinteractive
と関数message
の使い方を
multiply-by-seven
で見てみよう。
関数定義はつぎのとおりであった。
(defun multiply-by-seven (number) ; 対話的版 "Multiply NUMBER by seven." (interactive "p") (message "The result is %d" (* 7 number)))
この関数では、式(interactive "p")
は、要素2個のリストである。
"p"
は、前置引数を関数に渡すことをEmacsに指示するもので、
その値を関数への引数として使う。
引数は数である。
つまり、つぎの行のシンボルnumber
には数が束縛される。
(message "The result is %d" (* 7 number))
たとえば、前置引数が5であったとすると、 Lispインタープリタはつぎのような行であるとして評価する (GNU Emacsで読んでいる場合には、読者自身がこの式を評価してほしい)。
(message "The result is %d" (* 7 5))
まず、インタープリタは内側のリスト(* 7 5)
を評価する。
値35が返される。
つぎに、インタープリタは外側のリストを評価するが、それには、
リストの第2要素以降の値を関数message
に渡す。
すでに説明したように、message
はユーザーに1行のメッセージを
表示するために考えられたEmacs Lispの関数である
(See message)。
すなわち、関数message
は、%d
や%s
や%c
を除いて、
第1引数を字面のとおりにエコー領域に表示する。
%d
や%s
や%c
の制御文字列があると、
2番目以降の引数を調べてその値を対応する制御文字列の位置に表示する。
対話的関数multiply-by-seven
では、
制御文字列としては%d
を使っており、これは数を必要とする。
(* 7 5)
を評価した結果は数35である。
したがって、%d
の位置に数35が表示されるので、
メッセージはThe result is 35
となる。
(関数multiply-by-seven
を呼んだときは、
メッセージには二重引用符が付かないが、message
を呼んだときには、
二重引用符のあいだにテキストが表示されることに注意してほしい。
message
を先頭要素とする式を評価したときには、
message
が返した値がエコー領域に表示されるが、
関数内で用いた場合には、副作用としてmessage
が二重引用符なしでテキストを
表示するからである。)
interactive
の他のオプション上の例のmultiply-by-seven
では、
interactive
の引数として"p"
を用いた。
この引数は、ユーザーがタイプしたC-uや<META>に続く数を、
関数への引数として渡すコマンドとして解釈するようにEmacsに指示する。
Emacsでは、あらかじめ定義された20個以上の文字をinteractive
に指定できる。
ほとんどの場合、これらのオプションの数個を指定すれば、
必要な情報を関数へ対話的に渡せる
(See Interactive Codes)。
たとえば、文字r
を指定すると、Emacsは、リージョンの開始位置と終了位置
(ポイントとマークの現在値)を2つの引数として関数へ渡す。
つぎのように使う。
(interactive "r")
一方、B
を指定すると、Emacsは関数にバッファ名を渡す。
この場合、Emacsは、"BAppend to buffer: "
のようなB
に
続く文字列をプロンプトとしてミニバッファに表示して、
ユーザーに名前を問い合わせる。
Emacsはプロンプトを表示するだけでなく、<TAB>が押されると名前を補完する。
2つ以上の引数を取る関数では、
interactive
に続く文字列に要素を追加すれば各引数に情報を渡せる。
このとき、各引数に情報を渡す順番は、
リストinteractive
に指定した順番と同じである。
文字列の各部分は、改行\n
で区切る。
たとえば、"BAppend to buffer: "
に続けて、\n
とr
を
指定する。
こうすると、Emacsはバッファ名を問い合わせることに加えて、
ポイントとマークの値を関数に渡す。
つまり、引数は全部で3つである。
この場合、関数定義はつぎのようになり、
buffer
、start
、end
の各シンボルには、
バッファ、リージョンの開始位置、
終了位置の現在値をinteractive
が束縛する。
(defun 関数名 (buffer start end) "説明文..." (interactive "BAppend to buffer: \nr") 関数の本体...)
(プロンプトのコロンのうしろの空白は、
プロンプトを表示したときに見やすくするためのものである。
関数append-to-buffer
でもこのようにしている。
See append-to-bufferm。)
引数がない関数の場合には、interactive
には何も指定しなくてよい。
そのような関数では、単に式(interactive)
を指定する。
関数mark-whole-buffer
は、このようになっている。
読者のアプリケーションにおいて、あらかじめ定義した文字では不十分な場合には、
interactive
にリストを指定すれば、独自の引数を渡せる。
この技法について詳しくは、See interactive。
関数定義を評価して関数定義をインストールすると、 Emacsを終了するまでは関数定義は有効である。 新たにEmacsを起動したときには、 関数定義を再度評価するまでは関数定義はインストールされない。
新たにEmacsを起動するたびに、自動的にコードをインストールしたくなるであろう。 これにはいくつかの方法がある。
.emacs
に関数定義のコードを入れておく。
Emacsを起動すると、個人用の.emacs
が自動的に評価され、
その中のすべての関数定義がインストールされる。
See Emacs Initialization。
load
を用いてEmacsに各ファイルの関数定義を評価させインストールする。
See Loading Files。
site-init.el
にコードを収める。
こうすると、サイトのユーザーなら誰でもコードを使えるようになる
(Emacsのディストリビューションに含まれるファイルINSTALL
を参照のこと)。
最後に、Emacsのすべてのユーザーが使いそうなコードならば、 ネットワークに投稿するかFree Software Foundationに送付する (こうする場合には、投稿するまえにコードに コピーレフトの注意書きを入れてほしい)。 Free Software Foundationにコードを送付すると、 Emacsの次期リリースにコードが含まれるかもしれない。 このような「寄贈行為」によりEmacsが成長してきたのである。
let
let
式はLispのスペシャルフォームであり、
ほとんどの関数定義で使う必要がある。
let
はよく使うので、本節で説明しておく。
let
はシンボルに値を結び付ける、すなわち、束縛するのであるが、
関数の内部と外部で同じ名前の変数を使ってもLispインタープリタが混乱しない
ような方法でこれを行う。
このスペシャルフォームが必要な理由を理解するために、
「家を塗装し直す必要がある」などのように一般的に「家」と呼ぶような
家屋を所有している状況を仮定してみよう。
読者が友人宅を訪問したときに、友人が「家」といったときには、
友人の家を意味しているのであって、読者の家ではないだろう。
友人は彼の家を意味しているのに、読者が読者の家を意味していると考えると、
混乱が生じる。
ある関数の内部で使う変数と別の関数の内部で使う変数が同じ名前でも、
同じ値を参照する意図がないのであれば、Lispでも同じことが起こる。
スペシャルフォームlet
はこの種の混乱を防ぐ。
let
は、let
式の外側にある同じ名前を隠すような
ローカル変数(local variable)としての名前を作り出す。
これは、友人が「家」といった場合、彼は読者のではなく彼の家を意味していると
理解することに似ている
(引数リストに使われるシンボルも同じように働く。
See defun)。
let
式が作るローカル変数は、let
式の内側
(とlet
式から呼ばれた式の内側)でのみそれらの値を保持する。
ローカル変数は、let
式の外側にはまったく影響しない。
let
では一度に複数個の変数を作れる。
また、let
で各変数を作るときには、
指定した値かnil
を初期値として設定できる
(専門用語では、「変数に値を束縛する」という)。
let
で変数を作って束縛すると、let
の本体のコードを実行し、
let
式全体の値として本体の最後の式の値を返す
(「実行(execute)」とは、リストを評価することを意味する専門用語である。
これは、「実質的な効果を与える」という単語の用法
(Oxford English Dictionary)からきている。
ある動作を行わせるために式を評価するので、
「実行(execute)」は「評価(evaluate)」の同義語である)。
let
式の構造let
式は、3つの要素からなるリストである。
第一の部分は、シンボルlet
である。
第二の部分は、変数リスト(varlist)と呼ばれるリストであり、
その個々の要素は、単独のシンボルであるか、第1要素がシンボルであるような
2要素リストである。
let
式の第3の部分は、let
の本体である。
通常、本体は複数個のリストである。
let
式の雛型はつぎのとおりである。
(let 変数リスト 本体...)
変数リストの各シンボルは、
スペシャルフォームlet
で初期値を設定された変数である。
単独のシンボルの場合、初期値はnil
である。
第1要素がシンボルであるような2要素リストである場合、
そのシンボルには、Lispインタープリタが第2要素を評価した結果を束縛する。
したがって、変数リストは(thread (needles 3))
のようになる。
このlet
式では、Emacsは、シンボルthread
には初期値nil
を、
シンボルneedles
には初期値3を束縛する。
let
式を書くときには、
let
式の雛型の適当な項目に必要な式を書き込めばよい。
変数リストが2要素リストだけから成る場合には、
let
式の雛型はつぎのようになる。
(let ((変数 値) (変数 値) ...) 本体...)
let
式の例つぎの式では、2つの変数zebra
とtiger
を作り、
それぞれに初期値を与える。
let
式の本体は、関数message
を呼ぶリストである。
(let ((zebra 'stripes) (tiger 'fierce)) (message "One kind of animal has %s and another is %s." zebra tiger))
変数リストは((zebra 'stripes) (tiger 'fierce))
である。
2つの変数は、zebra
とtiger
である。
各変数は2要素リストの先頭要素であり、個々の値は2要素リストの第2要素である。
変数リストでは、Emacsは、変数zebra
には値stripes
を、
変数tiger
には値fierce
を束縛する。
この例では、どちらの値も引用符を直前に付けたシンボルである。
これらの値は、リストであっても文字列であってもよい。
変数を保持するリストのあとには、let
式の本体が続く。
この例では、エコー領域に文字列を表示する関数message
を使ったリストが
本体である。
これまでのように、例の最後の括弧の直後にカーソルを置いてC-x C-eと タイプすれば例を評価できる。 そうすると、エコー領域にはつぎのように表示されるはずである。
"One kind of animal has stripes and another is fierce."
これまでに見てきたように、関数message
は
%s
を除いて第1引数を表示する。
この例では、変数zebra
の値が最初の%s
の位置に、
変数tiger
の値が2番目の%s
の位置に表示される。
let
式の非初期化変数let
式において特に初期値を束縛していない変数には、
自動的に初期値としてnil
を束縛する。
つぎの例を見てほしい。
(let ((birch 3) pine fir (oak 'some)) (message "Here are %d variables with %s, %s, and %s value." birch pine fir oak))
変数リストは((birch 3) pine fir (oak 'some))
である。
いつものようにこの式を評価すると、エコー領域にはつぎのように表示される。
"Here are 3 variables with nil, nil, and some value."
この例では、Emacsは、シンボルbirch
に数3を、
シンボルpine
とfir
にnil
を、
シンボルoak
にsome
を束縛する。
let
の最初の部分では、変数pine
とfir
は括弧で囲んでない
単独のアトムである。
そのため、これらの変数は空リストnil
に束縛される。
一方、oak
は、リスト(oak 'some)
の一部なので、
some
に束縛される。
同様に、birch
もリストの一部なので数3に束縛される
(数はそれ自身に評価されるので、数をクオートする必要はない。
また、メッセージに数を表示するには%s
のかわりに%d
を使う)。
4つの変数をまとめてリストにすることで、let
の本体と区別できるようにする。
if
defun
やlet
に続く3番目のスペシャルフォームは、
条件分岐if
である。
このフォームは、コンピュータに判定を指示する。
if
を使わずに関数定義を書くことも可能であろうが、
多くの場面で使用する重要なものなので、ここで説明しておこう。
たとえば、関数beginning-of-buffer
のコードで使っている。
if
の基本的な考え方は、
「もし(if)条件が真ならば(then)式を評価する」である。
条件が真でなければ、式を評価しない。
たとえば、「もし(if)暑くて夏ならば(then)海へ行く!」のような判定に使う。
Lispでif
式を書く場合には、「then」を書かない。
第1要素がif
であるリストの第2要素と第3要素のそれぞれに、
判定条件と真の場合の動作を指定する。
if
式の条件を調べる部分を判定条件(if-part)、
2番目の引数を真の場合の動作(then-part)と呼ぶ。
また、if
式を書くとき、判定条件はシンボルif
と同じ行に書くが、
真の場合の動作は2行目以降に書く。
このようにするとif
式が読みやすくなる。
(if 判定条件 真の場合の動作)
判定条件は、Lispインタープリタが評価できればどんな式でもよい。
いつものようにして評価できる例をつぎにあげよう。
判定条件は、「数5は数4よりも大きいか」である。
これは真なので、メッセージ5 is greater than 4!
が表示される。
(if (> 5 4) ; 判定条件 (message "5 is greater than 4!")) ; 真の場合の動作
(関数>
は、第1引数が第2引数よりも大きいかどうかを調べ、
そうならば真を返す。)
実際のコードでは、if
式の判定条件は、式(> 5 4)
のように
固定されていない。
判定条件に使われる少なくとも1つの変数に束縛された値は、
あらかじめわかっていないはずである
(あらかじめ値がわかっていれば、テストする必要はない)。
たとえば、関数定義の引数に束縛された値を使う。
つぎの関数定義では、関数に渡される値は動物の性質である。
characteristic
に束縛された値がfierce
(獰猛な)の場合には、
メッセージIt's a tiger!
を表示する。
そうでなければ、nil
を返す。
(defun type-of-animal (characteristic) "Print message in echo area depending on CHARACTERISTIC. If the CHARACTERISTIC is the symbol `fierce', then warn of a tiger." (if (equal characteristic 'fierce) (message "It's a tiger!")))
GNU Emacsで読んでいる場合には、これまでのように関数定義を評価して 定義をEmacsにインストールし、つぎの2つの式を評価して結果を確認できる。
(type-of-animal 'fierce) (type-of-animal 'zebra)
(type-of-animal 'fierce)
を評価すると、
エコー領域にはメッセージ"It's a tiger!"
が表示される。
(type-of-animal 'zebra)
を評価すると、
エコー領域にはnil
と表示される。
if
expression.
type-of-animal
の詳細関数type-of-animal
を詳しく見てみよう。
type-of-animal
の関数定義は、
関数定義の雛型とif
式の雛型を埋めて書いたものである。
これらは対話的関数の雛型ではない。
(defun 関数名 (引数リスト) "説明文..." 本体...)
この雛型に対応する関数の部分はつぎのとおりである。
(defun type-of-animal (characteristic)
"Print message in echo area depending on CHARACTERISTIC.
If the CHARACTERISTIC is the symbol `fierce',
then warn of a tiger."
本体( if
式))
つまり、関数名はtype-of-animal
であり、渡される引数は1つである。
引数リストのあとには複数行の説明文字列が続いている。
各関数定義に説明文を付加しておくのはよい習慣なので、
この例でも説明文を付けておいた。
関数定義の本体はif
式から成る。
if
式の雛型はつぎのとおりである。
(if 判定条件 真の場合の動作)
関数type-of-animal
の実際のif
のコードはつぎのとおりである。
(if (equal characteristic 'fierce) (message "It's a tiger!")))
ここで、判定条件はつぎのとおり。
(equal characteristic 'fierce)
Lispでは、equal
は、第1引数が第2引数に等しいかどうかを調べる関数である。
第2引数はクオートしたシンボル'fierce
であり、
第1引数はシンボルcharacteristic
の値、
つまり、この関数に渡された引数である。
type-of-animal
の最初の使用例では、
引数fierce
をtype-of-animal
に渡した。
fierce
はfierce
に等しいので、
式(equal characteristic 'fierce)
は真を返す。
すると、if
は第2引数、つまり、
真の場合の動作(message "It's a tiger!")
を評価する。
一方、type-of-animal
の2番目の使用例では、
引数zebra
をtype-of-animal
に渡した。
zebra
はfierce
に等しくないので、真の場合の動作は評価されず、
if
式はnil
を返す。
if
式には第3引数を指定することもでき、判定条件が
偽の場合の動作(else-part)である。
判定条件が偽であると、if
式の第2引数、つまり、真の場合の動作は
いっさい評価されず、第3引数、つまり、偽の場合の動作が評価される。
曇の場合を考慮して
「もし(if)暑くて夏ならば(then)海へ行く、そうでなければ(else)読書する!」
のような判定である。
Lispのコードには「else」を書かない。
偽の場合の動作は、if
式の真の場合の動作のうしろに書く。
偽の場合の動作は新しい行で始め、真の場合の動作よりも字下げを少なくする。
(if 判定条件 真の場合の動作) 偽の場合の動作)
たとえば、つぎのif
式では、いつものように評価するとメッセージ
4 is not greater than 5!
を表示する。
(if (> 4 5) ; 判定条件 (message "5 is greater than 4!") ; 真の場合の動作 (message "4 is not greater than 5!")) ; 偽の場合の動作
適当に字下げすると真の場合の動作と偽の場合の動作を
区別しやすくなることに注意してほしい
(GNU Emacsには、if
式を自動的に正しく字下げするコマンドがある。
See Typing Lists)。
if
式に偽の場合の動作を追加するだけで、
関数type-of-animal
の機能を拡張できる。
関数type-of-animal
のつぎの版を評価して定義をインストールしてから、
続く2つの式を評価するとこの拡張を理解できるであろう。
(defun type-of-animal (characteristic) ; 第2版 "Print message in echo area depending on CHARACTERISTIC. If the CHARACTERISTIC is the symbol `fierce', then warn of a tiger; else say it's not fierce." (if (equal characteristic 'fierce) (message "It's a tiger!") (message "It's not fierce!")))
(type-of-animal 'fierce) (type-of-animal 'zebra)
(type-of-animal 'fierce)
を評価すると、
エコー領域にメッセージ"It's a tiger!"
が表示される。
ところが、(type-of-animal 'zebra)
を評価すると
"It's not fierce!"
と表示される。
(characteristicがferocious
(凶暴な)であれば、
メッセージ"It's not fierce!"
が表示されるが、これは誤解を招く。
コードを書く際には、if
で調べる値の可能な組み合わせを十分に考慮し、
そのようにプログラムを書く必要がある。)
if
式での判定条件が真かどうかの検査には重要な側面がある。
これまで、述語の値としての「真(true)」と「偽(false)」を、
新たなLispオブジェクトであるかのように使ってきた。
実際には、「偽(false)」とは、すでに馴染みのあるnil
のことである。
これ以外は、たとえ何であれ、「真(true)」である。
判定条件の式では、評価結果がnil
以外の値であれば、
真(true)と解釈する。
いいかえれば、47などの数、"hello"
のような文字列、
(nil
ではない)flowers
などのシンボルやリスト、
バッファでさえも、真と解釈する。
これらの例を示すまえに、nil
について説明しておこう。
Lispでは、シンボルnil
には2つの意味がある。
第一に、空リストを意味する。
第二に、偽を意味し、判定条件が偽の場合に返される値でもある。
nil
は、空リスト()
ともnil
とも書ける。
Lispインタープリタにとっては、()
もnil
も同じである。
一方、人間向きには、偽はnil
と、空リストは()
と書く傾向がある。
Lispでは、nil
でない、つまり、空リストでない値は真と解釈する。
つまり、評価結果が空リスト以外であれば、if
式の判定条件は真になる。
たとえば、判定条件に数を書いた場合、数を評価するとその数そのものである。
したがって、if
式の判定条件は真になる。
式の評価結果がnil
つまり空リストの場合に限り、判定条件は偽になる。
つぎの2つの式を評価すると理解できるだろう。
最初の例では、if
式の判定条件として数4を評価するが、
その結果は数4である。
したがって、式の真の場合の動作が評価され、その結果が返される。
つまり、エコー領域にはtrue
と表示される。
2番目の例では、nil
は偽を意味するので、
偽の場合の動作が評価されその結果が返される。
エコー領域にはfalse
と表示される。
(if 4 'true 'false) (if nil 'true 'false)
判定結果として真を表す有用な値がない場合には、
Lispインタープリタは真としてシンボルt
を返す。
たとえば、つぎの例でわかるように、式(> 5 4)
を評価するとt
を返す。
(> 5 4)
一方、偽の判定結果としては、この関数はnil
を返す。
(> 4 5)
save-excursion
関数save-excursion
は、
本章で説明する4番目で最後のスペシャルフォームである。
エディタとしてのEmacs Lispプログラムでは、
関数save-excursion
を多用している。
この関数は、ポイントとマークの位置を記録してから、関数の本体を実行し、
ポイントやマークの位置が移動していれば実行前の状態に復元する。
この関数の主要な目的は、ポイントやマークの予期しない移動によって
ユーザーが混乱したり煩わされないようにすることである。
save-excursion
を説明するまえに、
GNU Emacsのポイントとマークについて復習しておこう。
ポイント(point)とは、カーソルの現在位置である。
カーソルがどこにあろうとも、それがポイントである。
端末画面上では、カーソルは文字に重なって表示されるが、
ポインタはその文字の直前にある。
Emacs Lispでは、ポイントは整数である。
バッファの最初の文字は1、つぎの文字は2と数える。
関数point
はカーソルの現在位置を数で返す。
各バッファごとに、個別のポイントがある。
マーク(mark)も、バッファ内の位置を表す。
C-<SPC>(set-mark-command
)などのコマンドで、値を設定する。
マークを設定してあれば、
コマンドC-x C-x(exchange-point-and-mark
)を用いて
カーソルをマークに移動するとともに、
カーソル移動前のポイント位置にマークを設定する。
さらに、別のマークが設定してあった場合には、
交換前のマークの位置をマークリングに保存する。
このようにして複数個のマーク位置を保存できる。
C-u C-<SPC>と数回タイプすると保存したマーク位置に移動できる。
バッファのポイントとマークのあいだの部分をリージョン(region)と呼ぶ。
center-region
、count-lines-region
、kill-region
、
print-region
などのさまざまなコマンドはリージョンに作用する。
スペシャルフォームsave-excursion
は、ポイントとマークの位置を記録し、
Lispインタープリタがスペシャルフォームの本体のコードを評価し終えると、
それらの位置を復元する。
したがって、テキストの始めの部分にポイントがあったときに、
コードでポイントをバッファの最後に移動したとすると、
save-excursion
は、関数の本体の式を評価し終えると
ポイントをもとの場所に戻す。
Emacsでは、ユーザーが意図しなくても、関数の内部動作の過程でポイントを
移動することが多い。
たとえば、count-lines-region
はポイントを移動する。
(ユーザーの視点からは)予期しないような不必要なポイントの移動で
ユーザーが混乱しないように、ポイントやマークがユーザーの期待どおりの位置に
あるようにsave-excursion
を多用する。
save-excursion
を使うと、正しく管理できる。
正しく管理できるように、save-excursion
の内側のコードで
何か不都合なことが起こった場合(専門用語でいえば、「異常終了した場合」)でも、
save-excursion
はポイントとマークの値を復元する。
この機能はとても役に立つ。
ポイントとマークの値を記録することに加えて、
save-excursion
は、カレントバッファも記録しておいて復元する。
つまり、バッファを切り替えるようなコードを書いた場合でも、
save-excursion
によりもとのバッファに戻れる。
append-to-buffer
では、このためにsave-excursion
を使っている
(See append-to-buffer)。
save-excursion
式の雛型save-excursion
を使うコードの雛型は簡単である。
(save-excursion 本体...)
関数の本体は、複数個の式であり、Lispインタープリタはそれらを順に評価する。
本体に複数個の式がある場合、
最後の式の値が関数save-excursion
の値として返される。
本体のそれ以外の式は、副作用を得るためだけに評価される。
save-excursion
自体も(ポイントとマークの位置を復元するという)
副作用を得るためだけに使われる。
save-excursion
式の雛型をより詳しく書くと、つぎのようになる。
(save-excursion 本体の最初の式 本体の2番目の式 本体の3番目の式 ... 本体の最後の式)
ここで、式は単一のシンボルやリストである。
Emacs Lispのコードでは、save-excursion
式はlet
式の
本体に現れることが多い。
つぎのようになる。
(let 変数リスト (save-excursion 本体...))
これまでの章では、多数の関数やスペシャルフォームを紹介してきた。 以下には、説明しなかった同種の関数も含めて概要を記しておく。
eval-last-sexp
defun
例:
(defun back-to-indentation () "Point to first visible character on line." (interactive) (beginning-of-line 1) (skip-chars-forward " \t"))
interactive
\n
で区切る。
よく使うコード文字はつぎのとおりである。
b
f
p
r
コード文字の完全な一覧に
ついては、See Interactive Codes。
let
let
の本体で使用する変数のリストを宣言し、
それらにnil
や指定した値を初期値として設定する。
続いて、let
の本体の式を評価し、その最後の値を返す。
let
の本体の内側では、Lispインタープリタはlet
の外側で
同じ名前の変数に束縛された値を使うことはない。
例:
(let ((foo (buffer-name)) (bar (buffer-size))) (message "This buffer is %s and has %d characters." foo bar))
save-excursion
例:
(message "We are %d characters into this buffer." (- (point) (save-excursion (goto-char (point-min)) (point))))
if
スペシャルフォームif
は、条件判定(conditional)である。
Emacsには別の条件判定もあるが、もっともよく使うのはif
であろう。
例:
(if (string= (int-to-string 19) (substring (emacs-version) 10 12)) (message "This is version 19 Emacs") (message "This is not version 19 Emacs"))
equal
eq
equal
は、2つのオブジェクトが同じ内容で同じ構造ならば真を返す。
一方、eq
は、2つの引数が同一のオブジェクトならば真を返す。
<
>
<=
>=
<
は、第1引数が第2引数より小さいかどうかを検査する。
対応する関数>
は、第1引数が第2引数より大きいかどうかを検査する。
同様に、<=
は、第1引数が第2引数より小さいか等しいかどうかを検査し、
>=
は、第1引数が第2引数より大きいか等しいかどうかを検査する。
いずれの場合でも、2つの引数は数である必要がある。
message
%s
や%d
や%c
を含んでもよい。
%s
で使う引数は文字列かシンボルであること。
%d
で使う引数は数であること。
%c
で使う引数も数であるが、
その値のASCIIコードの文字として表示される。
setq
set
setq
は、第2引数の値を第1引数の値として設定する。
第1引数は自動的にクオートされる。
連続する2つの引数ごとに同じことを行う。
もう一方の関数set
は、2つの引数のみを取り、
両者を評価してから、第2引数の値を第1引数の値として設定する。
buffer-name
buffer-file-name
current-buffer
other-buffer
other-buffer
に引数として渡したバッファやカレントバッファ以外の)
もっとも最近に選択していたバッファを返す。
switch-to-buffer
set-buffer
buffer-size
point
point-min
point-max
fill-column
の現在の値が関数に渡した引数より大きいかどうか調べ、
そうならば適切なメッセージを表示するような関数を書いてみよ。
本章では、GNU Emacsで使われている数個の関数を詳しく調べよう。 つまり「ウォークスルー」をしてみよう。 これらの関数はLispコードの例題として取り上げたが、仮想的な例題ではない。 最初の簡略した関数定義を除いて、GNU Emacsで実際に使っているコードである。 これらの定義から多くのことを学べるはずである。 ここで説明する関数は、すべて、バッファに関連するものである。 それ以外の関数についてはのちほど説明する。
goto-char
,
point-min
, and push-mark
.
beginning-of-buffer
.
save-excursion
and
insert-buffer-substring
.
このウォークスルーでは、個々の新しい関数を、あるときは詳しく、 あるときはその概要を説明する。 Emacs Lisp関数に興味を持ったときには、 C-h fに続けて関数名(と<RET>)を入力すれば、 いつでもその関数の説明文を得られる。 同様に、変数の説明文が必要な場合には、C-h vに続けて 変数名(と<RET>)を入力すればよい。
また、関数のソースファイルを見るには、関数find-tags
を使う。
M-.(つまり、<META>と同時にピリオドキーを押すか、
<ESC>キーに続けてピリオドキー)をタイプし、
mark-whole-buffer
のようなソースコードを見たい関数の名前を
プロンプトに対して入力し、<RET>をタイプする。
Emacsはソースコードのバッファに切り替えて画面に表示する。
もとのバッファに戻るにはC-x b <RET>とタイプする。
読者のEmacsの初期設定状態に依存して、
TAGS
と呼ばれる「タグテーブル(tags table)」を指定する必要もあろう。
読者が使用するのはディレクトリemacs/src
であろうから、
コマンドM-x visit-tags-table
を使って、
/usr/local/lib/emacs/19.23/src/TAGS
のようなパス名を指定する。
読者独自のタグテーブルの作成方法については、
etagsや
See Tags。
Emacs Lispに慣れてくると、ソースコードを読む際にはfind-tags
を多用する
ようになり、独自のタグテーブルを作成するようになるであろう。
Lispコードを収めたファイルのことをライブラリ(libraries)と呼ぶ。
この用法は、法律図書や技術図書のような特化した図書(ライブラリ)からきている。
各ライブラリ、つまり、ファイルには、
ある特定の目的や動作に関連する関数群を収める。
たとえば、abbrev.el
は省略入力を、
help.el
はオンラインヘルプを扱うものである
(ある1つの目的のために複数のライブラリがある場合もある。
たとえば、rmail...
のファイル群には、
電子メールを読むためのコードが収めてある)。
GNU Emacsマニュアルには、「コマンドC-h pにより、
キーワードでEmacs Lispの標準ライブラリを検索できる」のような記述がある。
beginning-of-buffer
の簡略した定義コマンドbeginning-of-buffer
には十分慣れていて理解しやすいであろうから、
この関数から始めよう。
対話的関数として使うと、beginning-of-buffer
は、
バッファの先頭にカーソルを移動し、それまでカーソルがあった位置にマークを
設定する。
一般にはM-<にバインドしてある。
本節では、もっとも多く使われる形式に簡略した関数を説明する。 簡略版の関数は正しく動作するが、複雑なオプションを処理するコードは含まない。 別の節で、完全な関数を説明する (See beginning-of-buffer)。
コードを調べるまえに、関数定義には何が含まれるかを考えてみよう。 M-x beginning-of-bufferやM-<のようなキー列で関数を呼べるように、 関数を対話的にする式を含んでいる必要がある。 バッファのもとの位置にマークを設定するコードが必要である。 バッファの先頭にカーソルを移動するコードも必要である。
では、簡略版の関数のコード全体を示そう。
(defun simplified-beginning-of-buffer () "Move point to the beginning of the buffer; leave mark at previous position." (interactive) (push-mark) (goto-char (point-min)))
すべての関数定義と同様に、この定義でもスペシャルフォームdefun
に
続けて5つの部分がある。
simplified-beginning-of-buffer
である。
()
である。
この関数定義では、引数リストは空である。 つまり、この関数は引数を必要としない (関数の完全な定義では、省略可能な引数を取る)。
interactive
式は、関数が対話的に使われることをEmacsに伝える。
simplified-beginning-of-buffer
は引数を必要としないので、
interactive
に引数はない。
関数の本体はつぎの2行である。
(push-mark) (goto-char (point-min))
最初の式は(push-mark)
である。
Lispインタープリタがこの式を評価すると、
カーソルがどこにあってもその現在位置にマークを設定する。
また、このマークの位置はマークリングに保存される。
つぎの行は(goto-char (point-min))
である。
この式はバッファで取りえるポイントの最小位置にカーソルを移動する。
つまり、カーソルの先頭(あるいは、ナロイングしている場合には、
バッファの参照可能な範囲の先頭。
See Narrowing & Widening)に移動する。
式(goto-char (point-min))
でカーソルをバッファの先頭に移動するまえに、
コマンドpush-mark
でカーソルの位置にマークを設定する。
そのため、必要ならば、C-x C-xとタイプすればもとの位置に戻れる。
以上が関数定義のすべてである。
goto-char
のような知らない関数に出会ったときには、
コマンドdescribe-function
を使えば、何をする関数かを調べることができる。
このコマンドを使うには、C-h fに続けて関数名を入力してから
<RET>を押す。
コマンドdescribe-function
は、関数の説明文字列を
ウィンドウ*Help*
に表示する。
たとえば、goto-char
の説明文はつぎのとおりである。
One arg, a number. Set point to that number. Beginning of buffer is position (point-min), end is (point-max).
(describe-function
のプロンプトには、
カーソルのまえにあるシンボルが取り込まれる。
したがって、関数の直後にカーソルを置いてC-h f <RET>とタイプすれば、
入力量を減らせる。)
end-of-buffer
の関数定義は、beginning-of-buffer
と同じように
書けるが、関数の本体には(goto-char (point-min))
のかわりに
(goto-char (point-max))
を使う。
mark-whole-buffer
の定義関数mark-whole-buffer
を理解するのは、
関数simplified-beginning-of-buffer
を理解するのと同じくらい容易である。
ここでは、簡略版ではなく完全な関数を見てみよう。
関数beginning-of-buffer
ほどは多用されないが、
関数mark-whole-buffer
も有用である。
関数mark-whole-buffer
は、
バッファの先頭にポイントを、バッファの最後にマークを置いて
バッファ全体をリージョンとする。
一般にはC-x hにバインドされる。
関数の完全なコードはつぎのとおりである。
(defun mark-whole-buffer () "Put point at beginning and mark at end of buffer." (interactive) (push-mark (point)) (push-mark (point-max)) (goto-char (point-min)))
他のすべての関数定義と同様に、関数mark-whole-buffer
は
関数定義の雛型にあてはまる。
雛型はつぎのとおりである。
(defun 関数名 (引数リスト) "説明文..." (interactive-expression...) 本体...)
この関数はつぎのように動作する。
関数名はmark-whole-buffer
である。
空の引数リスト()
がこれに続き、関数には引数を必要としないことを意味する。
さらに、説明文が続く。
つぎの行の式(interactive)
は、関数が対話的に使われることをEmacsに
指示する。
これらの詳細は、前節で述べた関数simplified-beginning-of-buffer
と
同様である。
mark-whole-buffer
の本体関数mark-whole-buffer
の本体は、つぎの3行である。
(push-mark (point)) (push-mark (point-max)) (goto-char (point-min))
最初の行は、式(push-mark (point))
である。
この行は、関数simplified-beginning-of-buffer
の本体では
(push-mark)
と書かれた行とまったく同じことを行う。
いずれの場合も、Lispインタープリタはカーソルの現在位置にマークを設定する。
なぜ、mark-whole-buffer
では(push-mark (point))
と書き、
beginning-of-buffer
では(push-mark)
と書いたのかはわからない。
たぶん、コードを書いた人が、push-mark
の引数を省略でき、
引数がない場合にはpush-mark
はポイント位置に自動的にマークを
設定することを知らなかったのではないかと想像する。
あるいは、つぎの行と同じような構造にしたかったのであろう。
いずれにしても、この行により、Emacsはポイントの位置を調べて、
そこにマークを設定する。
mark-whole-buffer
のつぎの行は(push-mark (point-max))
である。
この式は、バッファで取り得るポイントの最大値の位置にマークを設定する。
これはバッファの最後である(バッファをナロイングしている場合には、
バッファの参照可能な部分の最後である。
ナロイングについてより詳しくは
See Narrowing & Widening)。
このマークを設定すると、ポイントに設定されていた直前のマークはなくなるが、
Emacsはその位置を最近の他のマークと同様に記録しておく。
つまり、必要ならばC-u C-<SPC>を2回タイプすれば
その位置に戻れるのである。
関数の最後の行は、(goto-char (point-min)))
である。
これはbeginning-of-buffer
のときとまったく同じに書いてある。
この式では、カーソルをバッファの最小ポイント
つまり、バッファの先頭(あるいは、バッファの参照可能な部分の先頭)に移動する。
この結果、バッファの先頭にポイントがあり、バッファの最後にマークが設定される。
したがって、バッファ全体がリージョンとなる。
append-to-buffer
の定義コマンドappend-to-buffer
は、コマンドmark-whole-buffer
と
同様に単純である。
このコマンドは、カレントバッファのリージョン(つまり、バッファのポイントと
マークのあいだの部分)を指定したバッファにコピーする。
コマンドappend-to-buffer
は、関数insert-buffer-substring
を
用いてリージョンをコピーする。
insert-buffer-substring
の名前からわかるように、
バッファのある部分を構成する文字列、つまり、「部分文字列」を
別のバッファに挿入する。
append-to-buffer
の大部分は、insert-buffer-substring
が動作する
ように条件を設定することである。
つまり、テキストを受け取るバッファとコピーすべきリージョンを
設定することである。
つぎは、関数の完全なテキストである。
(defun append-to-buffer (buffer start end) "Append to specified buffer the text of the region. It is inserted into that buffer before its point. When calling from a program, give three arguments: a buffer or the name of one, and two character numbers specifying the portion of the current buffer to be copied." (interactive "BAppend to buffer: \nr") (let ((oldbuf (current-buffer))) (save-excursion (set-buffer (get-buffer-create buffer)) (insert-buffer-substring oldbuf start end))))
この関数は、雛型を埋めたものと考えれば理解できるであろう。
もっとも外側の雛型は関数定義である。 ここでは、(いくつかの部分を埋めると)つぎのとおりである。
(defun append-to-buffer (buffer start end) "説明文..." (interactive "BAppend to buffer: \nr") 本体...)
関数の最初の行には、関数名と3つの引数がある。
引数は、テキストのコピーを受け取るバッファbuffer
、
カレントバッファのコピーすべきリージョンの
始めstart
と最後end
である。
関数のつぎの部分は説明文であるが、これは明白であろう。
let
expression.
save-excursion
works.
append-to-buffer
のinteractive
式関数append-to-buffer
は対話的に使われるので、
関数にはinteractive
式が必要である
(interactive
の復習は、
Interactiveを参照)。
この式はつぎのように読める。
(interactive "BAppend to buffer: \nr")
この式では、二重引用符のあいだに引数が、\n
で区切られて2つある。
最初の部分はBAppend to buffer:
である。
このB
は、関数にバッファ名を渡すことをEmacsに指示する。
Emacsは、B
に続く文字列Append to buffer:
をミニバッファに
表示してユーザーに名前を問い合わせる。
そして、Emacsは、指定されたバッファを
関数の引数リストの変数buffer
に束縛する。
改行\n
は引数の最初の部分と2番目の部分を区切る。
\n
に続くr
は、関数の引数リストのシンボル
buffer
に続く2つの引数(つまり、start
とend
)に
ポイントとマークの値を束縛するようにEmacsに指示する。
append-to-buffer
の本体関数append-to-buffer
の本体はlet
で始まる。
すでに説明したように(see let)、let
式の目的は、
let
の本体の内側のみで使う変数を作り初期値を設定することである。
つまり、let
式の外側で同じ名前の変数があっても、
それらとは混乱しないようにする。
let
式の概略を含んだappend-to-buffer
の雛型を示せば、
関数定義全体にどのようにlet
式をあてはめるかがわかるであろう。
(defun append-to-buffer (buffer start end) "説明文..." (interactive "BAppend to buffer: \nr") (let ((変数 値)) 本体...)
let
式には3つの要素がある。
let
。
(variable value)
。
let
式の本体。
関数append-to-buffer
では、変数リストはつぎのとおりである。
(oldbuf (current-buffer))
let
式のこの部分では、式(current-buffer)
が
返す値を変数oldbuf
に束縛する。
変数oldbuf
には、どのバッファを使用していたかを記録する。
Lispインタープリタが変数リストとlet
式の本体とを区別できるように、
変数リストの要素を括弧で囲む。
そのため、変数リストの2要素リストを括弧で囲むのである。
したがって、つぎのようになる。
(let ((oldbuf (current-buffer))) ... )
oldbuf
のまえにある2つの括弧のうち、
最初の括弧は変数リストの区切りを表し、
2番目の括弧は2要素リスト(oldbuf (current-buffer))
の始まりを表す
ことに注意してほしい。
append-to-buffer
のsave-excursion
append-to-buffer
のlet
式の本体は、
save-excursion
式から成っている。
関数save-excursion
は、ポイントとマークの位置を記録し、
save-excursion
の本体の式の実行を完了すると
これらの位置を復元する。
さらに、save-excursion
はもとのバッファも記録しておき復元する。
append-to-buffer
ではこのようにsave-excursion
を使う。
複数行にまたがるリストは、最初のシンボル以外は最初のシンボルよりも
字下げして書き、Lisp関数もこのように書く。
この関数定義では、つぎに示すように、let
はdefun
よりも字下げし、
save-excursion
はlet
よりも字下げする。
(defun ... ... ... (let... (save-excursion ...
このような書き方をすると、save-excursion
の本体の2行が
save-excursion
の括弧に囲まれていること、
また、save-excursion
自体もlet
の括弧に囲まれていること
がわかりやすくなる。
(let ((oldbuf (current-buffer))) (save-excursion (set-buffer (get-buffer-create buffer)) (insert-buffer-substring oldbuf start end))))
関数save-excursion
の使い方は、雛型の項目を埋めていくと考えればよい。
(save-excursion 本体の最初の式 本体の2番目の式 ... 本体の最後の式)
この関数では、save-excursion
の本体には2つの式があるだけである。
本体はつぎのとおりである。
(set-buffer (get-buffer-create buffer)) (insert-buffer-substring oldbuf start end)
関数append-to-buffer
を評価すると、
save-excursion
の本体の2つの式が順番に評価される。
最後の式の値が関数save-excursion
の値として返される。
これ以外の式は、副作用を起こすためだけに評価される。
save-excursion
の本体の最初の行では、
関数set-buffer
を用いてカレントバッファをappend-to-buffer
の
第1引数で指定したものに切り替える
(バッファを切り替えることは副作用である。
まえにも述べたように、Lispでは副作用が主要な目的である)。
2番目の行で、関数の主要な処理を行う。
関数set-buffer
は、Emacsの注意をテキストをコピーする先のバッファに向け、
save-excursion
でもとに戻す。
この行はつぎのとおりである。
(set-buffer (get-buffer-create buffer))
このリストのもっとも内側の式は(get-buffer-create buffer)
である。
この式では関数get-buffer-create
を使うが、
この関数は指定した名前のバッファを取得するか、
そのようなバッファが存在しなければその名前でバッファを作成する。
つまり、append-to-buffer
を使えば、
既存でないバッファにもテキストをコピーできる。
get-buffer-create
により、set-buffer
で不必要なエラーが
発生することを避けている。
set-buffer
には、切り替え先のバッファが必要である。
存在しないバッファを指定すると、Emacsは失敗する。
get-buffer-create
は、バッファが存在しなければバッファを作成するので、
set-buffer
にはつねにバッファが与えられる。
append-to-buffer
の最後の行が、テキストの追加操作を行う。
(insert-buffer-substring oldbuf start end)
関数insert-buffer-substring
は、第1引数で指定したバッファから
カレントバッファに文字列をコピーする。
ここでは、insert-buffer-substring
への引数は、
let
で作成し束縛した変数oldbuf
の値であり、
その値は、コマンドappend-to-buffer
を
実行したときのカレントバッファである。
insert-buffer-substring
の処理が終了すると、
save-excursion
はもとのバッファに戻し、
append-to-buffer
が終了する。
本体の動作の概要はつぎのとおりである。
(let (oldbuf
にcurrent-buffer
の値を束縛する) (save-excursion ; バッファを記録する バッファを切り替えるoldbuf
の文字列をバッファへコピーする) もとのバッファへ戻す 終了するときに名前oldbuf
を消す
まとめると、append-to-buffer
はつぎのように動作する。
変数oldbuf
にカレントバッファを記録する。
必要ならば作成して新しいバッファを取得し、そのバッファへ切り替える。
oldbuf
の値を用いて、もとのバッファのリージョンのテキストを
新しいバッファに挿入する。
save-excursion
を使っているので、もとのバッファに戻る。
append-to-buffer
を調べることで、ある程度複雑な関数について
知ることができた。
let
とsave-excursion
の使い方や、
バッファを切り替えたあとで、もとのバッファに戻す方法がわかったと思う。
多くの関数で、let
、save-excursion
、set-buffer
をこのように使う。
本章で説明したさまざまな関数の概要をまとめておく。
describe-function
describe-variable
find-tag
save-excursion
save-excursion
の引数を評価し終えたら、
もとの値に戻す。
カレントバッファも記録しておき、終了後にもとに戻す。
push-mark
goto-char
(point-min)
のような位置を返す式のいずれかである。
insert-buffer-substring
mark-whole-buffer
set-buffer
get-buffer-create
get-buffer
get-buffer
は、指定した名前のバッファが
存在しなければnil
を返す。
simplified-end-of-buffer
の関数定義を書き、動作を確認せよ。
if
とget-buffer
を用いて書け。
find-tag
を用いて、関数copy-to-buffer
のソースを探してみよ。
本章では、前章で学んだことをもとに、多少複雑な関数を見てみよう。
関数copy-to-buffer
では、1つの定義内で式save-excursion
を
2つ使う方法を、関数insert-buffer
では、
interactive
式での*
の使い方とor
の使い方を、
名前と名前が参照するオブジェクトとの重要な違いを示す。
set-buffer
, get-buffer-create
.
or
.
goto-char
,
point-min
, and push-mark
.
copy-to-buffer
の定義append-to-buffer
の動作を理解していれば、
copy-to-buffer
を理解するのは容易である。
この関数はテキストをバッファにコピーするが、
指定したバッファに追加するのではなく、指定バッファの内容を書き換える。
関数copy-to-buffer
のコードは、append-to-buffer
のコードと
ほぼ同じであるが、erase-buffer
ともう1つsave-excursion
を使う
(append-to-buffer
の説明は、
See append-to-buffer)。
copy-to-buffer
の本体はつぎのとおりである。
... (interactive "BCopy to buffer: \nr") (let ((oldbuf (current-buffer))) (save-excursion (set-buffer (get-buffer-create buffer)) (erase-buffer) (save-excursion (insert-buffer-substring oldbuf start end)))))
このコードは、append-to-buffer
のコードとほぼ同じである。
append-to-buffer
の定義と違いがでるのは、
コピー先のバッファに切り替えたあとである。
関数copy-to-buffer
では、バッファの既存の内容を削除する
(つまり、書き換えるのである。
Emacsでは、テキストを書き換えるには、既存のテキストを削除してから、
新たなテキストを挿入する)。
バッファの既存の内容を削除したあと、
2つめのsave-excursion
を使い、新たなテキストを挿入する。
なぜsave-excursion
を2つ使うのだろうか?
関数の動作を見直してみよう。
copy-to-buffer
の本体の概略はつぎのとおりである。
(let (oldbuf
にcurrent-buffer
の値を束縛する) (save-excursion ; 最初のsave-excursion
バッファを切り替える (erase-buffer) (save-excursion ; 2つめのsave-excursion
oldbuf
から部分文字列をバッファへ挿入する)))
最初のsave-excursion
で、Emacsは、テキストのコピーもとのバッファに
戻ることができる。
これはappend-to-buffer
での用法と同じであり、明らかであろう。
では、2つめは何のためであろう?
insert-buffer-substring
は、つねに、
挿入したリージョンの最後にポイントを置く。
2番目のsave-excursion
により、
Emacsは挿入したテキストの先頭にポイントを置くことになる。
多くの場合、ユーザーは、挿入したテキストの先頭にポイントがあることを好む
(もちろん、関数copy-to-buffer
が完了するともとのバッファに戻る。
しかし、ユーザーがコピー先のバッファに切り替えると、
ポイントはテキストの先頭にある。
つまり、このために2番目のsave-excursion
がある)。
insert-buffer
の定義insert-buffer
もバッファに関連した関数である。
このコマンドは、別のバッファからカレントバッファへコピーする。
カレントバッファのテキストのリージョンを別のバッファへコピーする
append-to-buffer
やcopy-to-buffer
とは逆向きである。
また、このコードでは、読み出し専用(read-only)のバッファでの
interactive
の使い方と、オブジェクトの名前と名前が参照する
オブジェクトとの重要な違いを示す。
コードはつぎのとおりである。
(defun insert-buffer (buffer) "Insert after point the contents of BUFFER. Puts mark after the inserted text. BUFFER may be a buffer or a buffer name." (interactive "*bInsert buffer: ") (or (bufferp buffer) (setq buffer (get-buffer buffer))) (let (start end newmark) (save-excursion (save-excursion (set-buffer buffer) (setq start (point-min) end (point-max))) (insert-buffer-substring buffer start end) (setq newmark (point))) (push-mark newmark)))
他の関数と同様に、雛型を使って関数の概略を見ることができる。
(defun insert-buffer (buffer) "説明文..." (interactive "*bInsert buffer: ") 本体...)
or
and a let
.
if
instead of an or
.
or
expression works.
save-excursion
expressions.
insert-buffer
のinteractive
式insert-buffer
では、interactive
の引数には、
アスタリスク*
とbInsert buffer:
の2つの部分がある。
アスタリスクは、読み出し専用バッファ、つまり、
変更できないバッファに対処するためである。
読み出し専用のバッファにinsert-buffer
を使うと、
読み出し専用である旨のメッセージがエコー領域に表示され、
端末のベルが鳴るか点滅する。
カレントバッファには挿入できないのである。
アスタリスクのあとにはつぎの部分との区切りに改行は必要ない。
interactive
式のb
interactive
式の引数の2番目の部分は、小文字のb
で始まっている
(大文字のB
で始まるappend-to-buffer
のコードと異なる
See append-to-buffer)。
小文字のb
は、insert-buffer
の引数は既存のバッファであるか
バッファ名であることをLispインタープリタに指示する
(大文字のB
では、バッファが存在しない場合も許す)。
Emacsは、デフォルトのバッファを提示してバッファ名を問い合わせ、
名前の補完も行う。
バッファが存在しないと、メッセージ「No match(一致するものがない)」を表示して
端末のベルを鳴らす。
insert-buffer
の本体関数insert-buffer
の本体には、2つの主要な部分、
or
式とlet
式がある。
or
式の目的は、引数buffer
がバッファの名前ではなく、
バッファに束縛されていることを保証することである。
let
式の本体は、
別のバッファからカレントバッファにコピーするコードである。
関数insert-buffer
の2つの式の概略はつぎのとおりである。
(defun insert-buffer (buffer)
"説明文..."
(interactive "*bInsert buffer: ")
(or ...
...
(let (変数リスト)
let
の本体... )
or
式により、どのようにして引数buffer
にバッファ名ではなく
バッファが束縛されることが保証されるのかを理解するには、
まず、関数or
を理解する必要がある。
そのまえに、関数の最初の部分をif
で書き換えて、
すでによく知っている方法で考えてみよう。
or
のかわりにif
を使ったinsert-buffer
必要なことは、buffer
の値をバッファ名ではなく
バッファそのものとすることである。
値が名前の場合には、対応するバッファそのものを取得する必要がある。
読者の名前が記された名簿を持って、会議場で受け付け係が読者を探している 場面を想像してほしい。 このとき受け付け係には、読者自身ではなく読者の名前が「束縛」されている。 受け付け係が読者を探しあてて読者の腕をとると、 受け付け係には読者が「束縛」される。
Lispでは、このような状況をつぎのように書ける。
(if (not (招待客の腕をとっている)) (招待客を探し、腕をとる))
バッファについても同じことをしたいのである。 バッファそのものを取得していなければ、それを取得したいのである。
(名前ではなく)バッファそのものを持っているかどうかを調べる
述語bufferp
を用いれば、つぎのようなコードが書ける。
(if (not (bufferp buffer)) ; 判定条件 (setq buffer (get-buffer buffer))) ; 真の場合の動作
ここで、if
式の判定条件は(not (bufferp buffer))
であり、
真の場合の動作は、式(setq buffer (get-buffer buffer))
である。
判定条件では、引数がバッファならば関数bufferp
は真を返すが、
引数がバッファ名の場合には偽を返す
(関数名bufferp
の最後の文字はp
である。
すでに説明したように、p
をそのように使うのは、
関数が述語(predicate)であることを意味する慣習である。
また、述語とは、ある性質が真であるか偽であるかを調べる関数であった。
See Wrong Type of Argument)。
式(bufferp buffer)
のまえには関数not
があり、
判定条件はつぎのとおりである。
(not (bufferp buffer))
not
は、引数が偽の場合には真を、真の場合には偽を返す関数である。
したがって、(bufferp buffer)
が真ならばnot
式は偽を返し、
偽ならば真を返す。
「not 真」は偽であり、「not 偽」は真である。
この判定条件を使うと、if
式はつぎのように動作する。
変数buffer
の値がバッファ名ではなく実際のバッファであれば、
判定条件は偽となり、if
式の真の場合の動作は評価されない。
変数buffer
が実際のバッファであれば何もする必要はないので、
これでよい。
一方、buffer
の値がバッファそのものではなくバッファ名の場合には、
判定条件は真を返し、真の場合の動作が評価される。
ここでは、真の場合の動作は(setq buffer (get-buffer buffer))
である。
この式では、バッファ名を与えると実際のバッファそのものを
返す関数get-buffer
を使う。
setq
により、変数buffer
にバッファそのものを設定し、
(バッファ名であった)まえの値を置き換える。
or
の本体関数insert-buffer
でのor
式の目的は、
引数buffer
にバッファ名ではなくバッファが束縛されていることを
保証することである。
上の節では、if
式を用いてこれを行う方法を示したが、
関数insert-buffer
では実際にはor
を使っている。
これを理解するには、or
の動作を理解する必要がある。
関数or
は、任意個数の引数を取る。
各引数を順番に評価し、nil
以外の値を返した最初の引数の値を返す。
さらに、nil
以外の値が初めて返されると、
それよりうしろの引数をいっさい評価しないのも、or
の重要な機能である。
or
式はつぎのようになる。
(or (bufferp buffer) (setq buffer (get-buffer buffer)))
or
の最初の引数は、式(bufferp buffer)
である。
この式は、バッファが名前ではなく実際のバッファであるときには
真(nil
以外の値)を返す。
or
式では、この場合にはor
式は真の値を返し、
そのつぎの式は評価しない。
buffer
の値が実際のバッファであれば何もする必要はないので、
これでよい。
一方、buffer
の値がバッファ名であると、
(bufferp buffer)
の値はnil
であり、
Lispインタープリタはor
式のつぎの要素を評価する。
つまり、式(setq buffer (get-buffer buffer))
を評価する。
この式は、nil
以外の値、つまり、変数buffer
に設定した値であり、
バッファ名ではなくバッファそのものである。
以上の効果をまとめると、シンボルbuffer
には、つねに、
バッファ名ではなくバッファそのものが束縛されるのである。
後続行の関数set-buffer
はバッファ名ではなくバッファそのものしか
扱えないので、以上の処理が必要なのである。
なお、or
を使えば、受け付け係の例はつぎのように書ける。
(or (招待客の腕をとっている) (招待客を探し腕をとる))
insert-buffer
のlet
式変数buffer
がバッファ名ではなくバッファそのものを参照することを
保証したあと、関数insert-buffer
はlet
式に進む。
ここには、3つのローカル変数、start
、end
、newmark
があり、
それぞれを初期値nil
に束縛する。
これらの変数はlet
内部の残りの部分で使われ、
let
が終わるまでEmacsの同じ名前の変数の出現を一時的に隠す。
let
の本体には2つのsave-excursion
がある。
まず、内側のsave-excursion
を詳しく見てみよう。
その式はつぎのとおりである。
(save-excursion (set-buffer buffer) (setq start (point-min) end (point-max)))
式(set-buffer buffer)
は、Emacsの注意をカレントバッファから
テキストのコピーもとのバッファに向ける。
そのバッファにて、コマンドpoint-min
とpoint-max
を用いて
バッファの先頭と最後を変数start
とend
に設定する。
ここでは、setq
により、1つの式で2つの変数に値を設定する方法も
示している。
setq
の最初の引数には第2引数の値が、第3引数には第4引数の値が設定される。
内側のsave-excursion
の本体を終了すると、
save-excursion
はもとのバッファに戻すが、
start
とend
には、
テキストのコピーもとのバッファの先頭と最後の値が設定されたままである。
外側のsave-excursion
式の概要はつぎのとおりである。
(save-excursion (内側の式save-excursion
(新しいバッファに切り替えstart
とend
を設定) (insert-buffer-substring buffer start end) (setq newmark (point)))
関数insert-buffer-substring
は、
バッファbuffer
の始めstart
から終わりend
までのリージョンの
テキストをカレントバッファにコピーする。
start
とend
のあいだは2番目のバッファ全体であるので、
2番目のバッファ全体が読者が編集しているバッファにコピーされる。
続いて、挿入したテキストの最後にあるポイントを変数newmark
に記録する。
外側のsave-excursion
の本体を評価し終えると、
ポイントとマークはもとの位置に戻される。
しかし、新たに挿入したテキストの最後にマークを、先頭にポイントを設定
しておくと便利である。
変数newmark
は挿入したテキストの最後を記録している。
let
式の最後の行の式(push-mark newmark)
で、
このようにマークを設定する
(そのまえのマークの位置も参照できる。
つまりマークリングに記録されているので、C-u C-<SPC>で戻れる)。
一方、ポイントは挿入したテキストの先頭にあり、
挿入する関数を呼ぶまえの位置のままである。
let
式全体はつぎのとおりである。
(let (start end newmark) (save-excursion (save-excursion (set-buffer buffer) (setq start (point-min) end (point-max))) (insert-buffer-substring buffer start end) (setq newmark (point))) (push-mark newmark))
関数append-to-buffer
と同様に、関数insert-buffer
は、
let
とsave-excursion
とset-buffer
を使っている。
さらに、この関数ではor
の1つの使い方を示した。
これらのすべての関数は、何度も何度も使う構成部品である。
beginning-of-buffer
の完全な定義関数beginning-of-buffer
の基本的な構造はすでに説明した
(See simplified-beginning-of-buffer)。
まえに説明したように、引数なしでbeginning-of-buffer
を起動すると、
カーソルをバッファの先頭に移動し、マークを移動前の位置に設定する。
ところが、1〜10までの数nを指定して起動すると、
関数はその数をバッファ長を単位としたn/10と解釈し、
Emacsはバッファの先頭からn/10の位置にカーソルを移動する。
したがって、この関数をM-<で呼べばバッファの先頭にカーソルを移動するし、
C-u 7M-<で呼べばバッファの70%のところにカーソルを移動する。
引数に10より大きな数を使うと、バッファの最後にカーソルを移動する。
関数beginning-of-buffer
は、引数を指定しても、しなくても呼べる。
つまり、引数は省略できる。
特に指定しない限り、Lispは、関数定義で引数を指定した関数は、
引数に渡す値とともに呼ばれることを期待する。
そうでないと、Wrong number of arguments
のエラーメッセージを得る。
しかし、オプション引数の機能がLispにはある。
引数を省略できることをキーワード(keyword)で
Lispインタープリタに指示する。
そのキーワードは&optional
である
(optional
の直前の&
もキーワードの一部である)。
関数定義において、キーワード&optional
のあとに続く引数には、
関数を呼び出すときにその引数に値が渡されなくてもよい。
したがって、beginning-of-buffer
の関数定義の最初の行はつぎのようになる。
(defun beginning-of-buffer (&optional arg)
関数全体の概略はつぎのとおりである。
(defun beginning-of-buffer (&optional arg) "説明文..." (interactive "P") (push-mark) (goto-char (引数があれば 移動先の位置を計算する さもなければ、 (point-min))))
この関数は、simplified-beginning-of-buffer
に似ているが、
interactive
式には引数として"P"
があり、
関数goto-char
に続けて、引数が指定された場合に
移動先を計算するif
式がある点が異なる。
interactive
式の"P"
は、前置引数がある場合にはそれを
関数に渡すことをEmacsに指示する。
前置引数は、<META>キーに続けて数をタイプするか、
C-uをタイプしてから数をタイプする
(数をタイプしないと、C-uのデフォルトは4である)。
if
式の判定条件は単純で、単に引数arg
である。
arg
にnil
以外の値があるのは、
引数を指定してbeginning-of-buffer
が呼ばれた場合であり、
if
式の真の場合の動作を評価する。
一方、引数なしでbeginning-of-buffer
を呼ぶと、
arg
の値はnil
となりif
式の偽の場合の動作が評価される。
偽の場合の動作は単純であり、point-min
である。
このように評価される場合は、goto-char
式全体は
(goto-char (point-min))
となり、
関数beginning-of-buffer
の簡略な場合と同じである。
beginning-of-buffer
引数を指定してbeginning-of-buffer
を呼ぶと、
goto-char
に渡す値を計算する式が評価される。
この式は、一見、複雑なように見える。
これには、内側にif
式や多くの算術演算がある。
つぎのとおりである。
(if (> (buffer-size) 10000) ;; バッファサイズが大きな場合の桁溢れを防ぐ! (* (prefix-numeric-value arg) (/ (buffer-size) 10)) (/ (+ 10 (* (buffer-size) (prefix-numeric-value arg))) 10))
複雑に見える他の式と同様に、この式も雛型に当てはめてみると解ける。
ここではif
式の雛型を使う。
この式の骨格はつぎのようになる。
(if (バッファが大きい バッファサイズを10で割ってから、引数を掛ける さもなければ、別の計算方法
内側のif
式の判定条件では、バッファサイズを調べる。
Emacs Lispの第18版では、(大きな数は必要ないので)8百万を超える数は扱えず、
バッファが大きな場合にEmacsがこの制限を超えるような数を
計算する可能性があるので、このような検査をする。
注釈中の「桁溢れ(overflow)」は、数が大きすぎることを意味する。
バッファが大きい場合とそうでない場合の2つの場合がある。
beginning-of-buffer
では、内側のif
式で
バッファサイズが10000文字より大きいかどうかを検査する。
これには関数>
と関数buffer-size
を使う。
つぎのとおりである。
(if (> (buffer-size) 10000)
バッファが大きな場合、if
式の真の場合の動作が評価される。
(読みやすいように整形すると)その行はつぎのようになる。
(* (prefix-numeric-value arg) (/ (buffer-size) 10))
この式は乗算であり、関数*
には2つの引数を渡す。
第1引数は(prefix-numeric-value arg)
である。
interactive
の引数に"P"
を使うと、
関数に引数として渡される値は数ではなく、「生の前置引数(raw prefix argument)」
である(数のリスト)。
数値演算を施すには、変換する必要があり、
それにはprefix-numeric-value
を使えばよい。
第2引数は(/ (buffer-size) 10)
である。
この式は、バッファの大きさの数を10で割る。
これにより、バッファサイズの1/10に相当する文字数が得られる
(Lispでは、*
を乗算に使うように、/
を除算に使う)。
乗算の式全体では、この値に引数の値を掛ける。 乗算はつぎのようになる。
(* 前置引数の値 バッファの文字数の1/10)
たとえば、前置引数が7
ならば、1/10に相当する値に7を掛け、
バッファの70%の位置になる。
つまり、バッファが大きな場合には、以上の結果から、
goto-char
式はつぎのようになる。
(goto-char (* (prefix-numeric-value arg) (/ (buffer-size) 10)))
これにより、望みの位置にカーソルが移動する。
バッファが10000文字未満の場合には、少々異なる計算を行う。 最初の計算方法で十分であり、このようなことは不要と考える読者もいよう。 しかし、小さなバッファでは、最初の計算方法では、目的の行に正しく カーソルを移動できず、2番目の計算方法のほうがよい。
コードはつぎのとおりである。
(/ (+ 10 (* (buffer-size) (prefix-numeric-value arg))) 10))
このコードでは、関数がどのように括弧の中に入れ子になっているかがわかれば、 何が起こるかを理解できる。 入れ子になった式をより深く字下げして整形すると読みやすくなる。
(/ (+ 10 (* (buffer-size) (prefix-numeric-value arg))) 10))
括弧をよく見ると、もっとも内側の演算は(prefix-numeric-value arg)
であり、
生の引数を数に変換する。
つぎの式で、この数にバッファサイズを掛ける。
(* (buffer-size) (prefix-numeric-value arg)
この乗算により、バッファのサイズよりも大きな数を得る。 たとえば、引数が7ならば、7倍大きい。 この数に10を加えてから10で割り、バッファの目的の位置より1だけ大きな数を得る。
この計算結果をgoto-char
に渡してカーソルを移動する。
beginning-of-buffer
の完全なコード関数beginning-of-buffer
の完全なテキストをつぎに示す。
(defun beginning-of-buffer (&optional arg) "Move point to the beginning of the buffer; leave mark at previous position. With arg N, put point N/10 of the way from the true beginning. Don't use this in Lisp programs! \(goto-char (point-min)) is faster and does not set the mark." (interactive "P") (push-mark) (goto-char (if arg (if (> (buffer-size) 10000) ;; Avoid overflow for large buffer sizes! (* (prefix-numeric-value arg) (/ (buffer-size) 10)) (/ (+ 10 (* (buffer-size) (prefix-numeric-value arg))) 10)) (point-min))) (if arg (forward-line 1)))
2つの小さな点を除いて、この関数はまえに述べたような動作をする。 最初の点は、説明文字列に関する部分であり、 第2の点は関数の最後の行である。
説明文字列では、つぎの式を参照している。
\(goto-char (point-min))
この式の最初の括弧のまえには\
がある。
この\
は、シンボリック式として評価するのではなく
説明文として式を表示するようにLispインタープリタに指示する。
コマンドbeginning-of-buffer
の最後の行は、
引数を指定してコマンドを起動した場合に、ポイントを次行の先頭に移動する。
(if arg (forward-line 1)))
これにより、バッファの適切なn/10の位置の直後の行の先頭にカーソルを置く。 これは、すくなくともバッファの指定されたn/10の位置にカーソルを位置決めして 見栄えをよくするためのもので、必要ないことかもしれないが、 こうしないと不満のもとになる。
本章で説明したことがらをまとめておく。
or
nil
以外の値を返した最初の値を返す。
nil
以外の値を返すものがなければ、nil
を返す。
つまり、引数の最初の真の値を返す。
1つでも真のものがあれば、真の値を返す。
and
nil
ならばnil
を返す。
nil
でないものがなければ、最後の引数の値を返す。
つまり、すべての引数が真ならば真の値を返す。
&optional
prefix-numeric-value
(interactive "P")
で得た「生の前置引数」を数値に変換する。
forward-line
forward-line
は可能なだけ進めて、指定量に満たない行数を返す。
erase-buffer
bufferp
t
を、さもなければnil
を返す。
&optional
引数の演習問題省略できる引数に指定した数がfill-column
の値に比べて
大きいか小さいかをメッセージに表示する対話的関数を書いてみよ。
ただし、関数に引数を渡されなかった場合のデフォルトは56とする。
ナロイングとは、バッファの特定部分に集中して残りの部分を 不用意に変更しないようにするEmacsの機能である。 初心者に混乱を与えないように、ナロイングは、通常、無効にしてある。
save-restriction
special form.
ナロイングすると、バッファの残りの部分は存在しないかのように見えなくなる。 たとえば、バッファのある部分のみで単語を置き換えるような場合には、 利点となる。 望みの範囲にナロイングすれば、その部分だけで置き換えを行える。 文字列検索もその範囲内のみで行えるので、 たとえば、文書のある部分を編集する場合、編集範囲にナロイングしておけば それ以外の範囲の文字列を拾うことがない。
しかし、ナロイングするとバッファの残りの部分が見えなくなるため、
偶然にナロイングしてしまうとファイルのその部分を削除してしまったと思うような
読者を怯えさせる。
さらに、(通常C-x uにバインドされる)コマンドundo
でも、
ナロイングを無効にしない(また、そうすべきでもない)ので、
コマンドwiden
によりバッファの残りの部分が見えるようになることを
知らない人は絶望することになる
(Emacsの第18版では、widen
はC-x wに、
第19版では、C-x n wにバインドしてある)。
ナロイングは人間にとってと同様にLispインタープリタにとっても有用である。
Emacs Lisp関数は、しばしば、バッファの一部分に働くように設計されている。
逆に、ナロイングしてあるバッファの全体に働く必要があるEmacs Lisp関数もある。
たとえば、関数what-line
は、ナロイングが有効な場合にはそれを無効にし、
処理を終了するともとのようにナロイングする。
一方で、what-line
からも呼ばれる関数count-lines
では、
ナロイングを用いてバッファの操作範囲を限定し、終了するときにもとに戻す。
save-restriction
Emacs Lispでは、スペシャルフォームsave-restriction
を用いて、
設定されているナロイングの範囲を記録できる。
Lispインタープリタがsave-restriction
式に出会うと、
save-restriction
の本体を実行し、それらがナロイング範囲を
変更した場合にはもとに戻す。
たとえば、バッファをナロイングしてあるときに、save-restriction
に続く
コードでナロイングを取り除いた場合には、完了後にsave-restriction
は
バッファのナロイングをもとに戻す。
コマンドwhat-line
では、
コマンドsave-restriction
の直後にあるコマンドwiden
で
バッファに設定されたナロイングを取り除く。
関数が終了する直前にもとのナロイングが復元される。
save-restriction
式の雛型は単純である。
(save-restriction 本体... )
save-restriction
の本体は、1つ以上の式であり、
Lispインタープリタが順番に評価する。
save-excursion
とsave-restriction
を続けて使う場合には、
save-excursion
は外側で使うべきである。
逆順に使うと、save-excursion
を呼んでEmacsが切り替えたバッファの
ナロイングを記録し損なうことがある。
つまり、save-excursion
とsave-restriction
とを一緒に使う場合には、
つぎのように書く。
(save-excursion (save-restriction 本体...))
what-line
コマンドwhat-line
は、カーソルが位置する行の行数を調べる。
この関数では、コマンドsave-restriction
とsave-excursion
の
使用例を示す。
関数の完全なテキストはつぎのとおりである。
(defun what-line () "Print the current line number (in the buffer) of point." (interactive) (save-restriction (widen) (save-excursion (beginning-of-line) (message "Line %d" (1+ (count-lines 1 (point)))))))
関数には、予想どおり、説明文とinteractive
式がある。
続く2つの行では関数save-restriction
とwiden
を使っている。
スペシャルフォームsave-restriction
は、
カレントバッファに設定されているナロイングを記録し、
save-restriction
の本体のコードを評価したあと、
記録したナロイングを復元する。
スペシャルフォームsave-restriction
に続いて、widen
がある。
この関数は、what-line
が呼ばれたときにカレントバッファに設定されている
ナロイングを無効にする
(このナロイングはsave-restriction
が記録している)。
このワイドニングにより、バッファの先頭から行数を数えられるようになる。
さもないと、参照可能なリージョン内に制限されてしまう。
設定されていたもとのナロイングは、
スペシャルフォームsave-restriction
を終了する直前に復元される。
widen
の呼び出しに続いて、save-excursion
があり、
カーソル(つまり、ポイント)とマークの位置を記録し、
save-excursion
の本体のコードでポイントを移動する
関数beginning-of-line
を呼んだあとにそれらを復元する。
(式(widen)
は、save-restriction
とsave-excursion
のあいだに
ある。
2つのsave- ...
式を続けて順に書く場合には、
save-excursion
を外側にすること。)
関数what-line
の最後の2行で、バッファの行数を数えて、
エコー領域にその数を表示する。
(message "Line %d" (1+ (count-lines 1 (point)))))))
関数message
はEmacsの画面の最下行に1行のメッセージを表示する。
第1引数は二重引用符のあいだにあり、文字列として表示される。
ただし、この文字列には、これに続く引数を表示するための
%d
や%s
や%c
が含まれてもよい。
%d
は、引数を10進数で表示するので、
メッセージはLine 243
のようになる。
%d
のかわりに表示される数は、関数のつぎの行で計算される。
(1+ (count-lines 1 (point)))
このコードは、1
で示されるバッファの先頭から(point)
までの
行数を数え、それに1を加える
(関数1+
は、引数に1を加える)。
1を加えるのは、たとえば、2番目の行のまえには1行しかないからであり、
count-lines
は現在行の直前までの行数を数える。
count-lines
が処理を終了するとエコー領域にメッセージが表示され、
save-excursion
がポイントとマークの位置をもとに戻す。
さらに、save-restriction
はもとのナロイングの設定を復元する。
バッファの後半にナロイングしていて最初の行を参照できないような場合であっても、
カレントバッファの最初の60文字を表示する関数を書いてみよ。
これには、save-restriction
、widen
、goto-char
、
point-min
、buffer-substring
、message
を始め、
さまざまな関数をポプリのように混ぜ合わせて使う必要がある。
car
、cdr
、cons
Lispでは、car
、cdr
、cons
は基本関数である。
関数cons
はリストの作成、
関数car
とcdr
はリストの分解に使う。
関数copy-region-as-kill
のウォークスルーでは、
cons
に加えてcdr
の変形であるsetcdr
とnthcdr
を
見ることになる
(See copy-region-as-kill)。
cdr
repeatedly.
関数cons
の名前は不合理ではなく、
単語「construct(作り上げる)」の省略である。
一方、car
とcdr
の名前の由来は、難解である。
car
は「Contents of the Address part of the Register」の頭文字であり、
cdr
(「クダー」と読む)は
「Contents of the Decrement part of the Register」の頭文字である。
これらの語句は、最初のLispが開発された初期のコンピュータの
ハードウェアの一部を指す。
この語句は古くて意味がないばかりでなく、
Lispに関していえば、25年間以上にもわたってこれらの語句は無意味であった。
研究者の一部には、これらの関数に対する合理的な名称を使う人もいるが、
それにもかかわらず、これらの名称は使われ続けている。
特に、これらはEmacs Lispのソースコードでも使われているので、
本書でもこれにならう。
car
とcdr
リストのcar
は、簡単にいえば、リストの先頭要素である。
したがって、リスト(rose violet daisy buttercup)
のcar
は、
rose
である。
GNU EmacsのInfoで読んでいる場合には、つぎを評価するとわかる。
(car '(rose violet daisy buttercup))
式を評価すると、エコー領域にrose
と表示される。
明らかに、関数car
のもっと合理的な名称はfirst
であり、
しばしばそのように提案されている。
car
は、リストからその先頭要素を取り除くのではない。
先頭要素が何であるかを返すだけである。
リストにcar
を適用したあとでも、リストはそれ以前と同じである。
専門用語では、car
は「非破壊的(non-destructive)」であるという。
この機能は重要なことがあとでわかる。
リストのcdr
は、リストの残りである。
つまり、関数cdr
は、リストの最初の要素のあとに続く部分を返す。
したがって、リスト'(rose violet daisy buttercup)
のcar
は、
rose
であるが、cdr
が返すリストの残りは
(violet daisy buttercup)
である。
いつものようにつぎの式を評価すればわかる。
(cdr '(rose violet daisy buttercup))
これを評価すると、エコー領域には(violet daisy buttercup)
と表示される。
car
と同様に、cdr
もリストから要素を取り除くことはない。
リストの第2要素以降が何であるかを返すだけである。
上の例では、花のリストをクオートしていた。
クオートしないと、Lispインタープリタは、関数としてrose
を呼び
リストを評価しようとする。
この例では、そのようにはしたくないのである。
明らかに、関数cdr
のより合理的な名称はrest
であろう。
(つぎのことに注意してほしい:
新しい関数に名前を付けるときには、何をしているかを注意深く考えてほしい。
というのは、予想以上に長期間にわたって使われることもあるからである。
本書で、(car
やcdr
のような)これらの名称を使い続けるのは、
Emacs Lispのソースコードでこれらを使っているからである。
同じ名称を使わないと、読者がコードを読む際に苦労するであろう。
合理的な名称を使えば、あとに続く人々に感謝されるはずである。)
(pine fir oak maple)
のようなシンボルだけから成るリストに
car
やcdr
を適用すると、関数car
が返す
リストの要素は周りに括弧のないシンボルpine
である。
pine
は、リストの先頭要素である。
一方、つぎの式を評価してみるとわかるように、
リストのcdr
はリストであり、(fir oak maple)
である。
(car '(pine fir oak maple)) (cdr '(pine fir oak maple))
ところが、リストのリストでは、先頭要素は、それ自体、リストである。
car
はリストの先頭要素をリストとして返す。
たとえば、3つのリスト、肉食獣のリスト、草食獣のリスト、海棲哺乳類のリスト
から成るリストを考える。
(car '((lion tiger cheetah) (gazelle antelope zebra) (whale dolphin seal)))
この場合、第1要素、つまり、リストのcar
は、
肉食獣のリスト(lion tiger cheetah)
であり、リストの残りの部分は
((gazelle antelope zebra) (whale dolphin seal))
である。
(cdr '((lion tiger cheetah) (gazelle antelope zebra) (whale dolphin seal)))
再度指摘するが、car
とcdr
は、非破壊的である。
つまり、これらをリストに適用したあとでも、
リストは変更されていないことに注意してほしい。
これらの使い方において、この性質はとても重要である。
第1章のアトムに関する説明で、Lispにおいては、
「配列などのある種のアトムは分解できるが、
その機構はリストを分解する機構とは異なる。
Lispでは、リストのアトムを分解することはできない」と述べた
(See Lisp Atoms)。
関数car
とcdr
は、リストを分解するために使い、
Lispにとって基本的であると考えられている。
これらの関数では、
配列を分解したりその一部を参照できないので、配列はアトムと考えられる。
逆に、別の基本関数cons
はリストを作り上げるが、配列を作ることはできない
(配列は、配列専用の関数で処理する。
See Arrays)。
cons
関数cons
はリストを作り、car
とcdr
の逆操作である。
たとえば、3要素リスト(fir oak maple)
から4要素リストを作るのに
cons
を使う。
(cons 'pine '(fir oak maple))
このリストを評価すると、つぎのリスト
(pine fir oak maple)
がエコー領域に表示される。
cons
は、リストの先頭に新たな要素を置く、あるいは、要素をリストに繋ぐ。
cons
には繋ぐべきリストが必要である。
3
何もないところから始めることはできない。
リストを作るときには、最初は少なくとも空リストを与える必要がある。
花のリストを作る一連のcons
をつぎに示す。
GNU EmacsのInfoで読んでいる場合には、いつものように各式を評価できる。
値は「の評価結果は」と読める=>
のうしろに記しておく。
(cons 'buttercup ()) => (buttercup) (cons 'daisy '(buttercup)) => (daisy buttercup) (cons 'violet '(daisy buttercup)) => (violet daisy buttercup) (cons 'rose '(violet daisy buttercup)) => (rose violet daisy buttercup)
最初の例では、空リストを()
で表し、buttercup
に空リストが続く
リストを作成している。
見ればわかるように、作成したリスト内の空リストは表示されない。
(buttercup)
とだけ表示される。
空リストには何も含まれないので、空リストをリストの要素としては数えない。
一般に、空リストは見えない。
2番目の例では、(cons 'daisy '(buttercup))
により、
buttercup
のまえにdaisy
を置いて新たに2要素リストを作る。
3番目の例では、daisy
とbuttercup
のまえにviolet
を置いて
3要素リストを作っている。
length
Lisp関数length
を使うとリスト内の要素の個数を調べることができる。
たとえば、つぎの式、
(length '(buttercup)) => 1 (length '(daisy buttercup)) => 2 (length (cons 'violet '(daisy buttercup))) => 3
3番目の例では、関数cons
を用いて3要素リストを作り、
それを関数length
の引数として渡している。
空リストの要素数を数える場合にもlength
を使える。
(length ()) => 0
予想どおりに、空リストの要素数は0である。
リスト以外の長さを調べようとするとどうなるであろうか?
length
に空リストさえも与えずに引数なしで呼んでみよう。
(length )
これを評価すると、つぎのエラーメッセージを得る。
Wrong number of arguments: #<subr length>, 0
これは、関数が予想する引数の個数とは異なる、
0個の引数を受け取ったことを意味する。
この場合は、関数length
が長さを調べる引数として1個必要である
(リストの要素数がいくつであろうが、
1つのリストは1つの引数である)。
エラーメッセージの#<subr length>
の部分は、関数名を表す。
これは特別な記法#<subr
で書かれており、
関数length
はEmacs LispではなくCで書かれた基本操作関数であることを
意味する
(subr
は、「subroutine(サブルーティン)」の略)。
サブルーティンについてより
詳しくは、See What Is a Function。
nthcdr
関数nthcdr
は、関数cdr
に関連しており、
リストのcdr
を繰り返し取る。
リスト(pine fir oak maple)
のcdr
を取ると、
リスト(fir oak maple)
を得る。
同じことを返された値に適用するとリスト(oak maple)
を得る
(もちろん、もとのリストにcdr
を繰り返し適用しても、
関数はリストを変更しないので、同じ結果を得るだけである。
cdr
のcdr
のように評価する必要がある)。
これを続けると、最終的には空リストを得ることになるが、
()
のかわりにnil
と表示される。
一連のcdr
を繰り返して適用してみよう。
それぞれの結果は、=>
のうしろに記しておく。
(cdr '(pine fir oak maple)) =>(fir oak maple) (cdr '(fir oak maple)) => (oak maple) (cdr '(oak maple)) =>(maple) (cdr '(maple)) => nil (cdr 'nil) => nil (cdr ()) => nil
一連のcdr
のあいだで値を表示しない場合には、つぎのようにする。
(cdr (cdr '(pine fir oak maple))) => (oak maple)
この例では、Lispインタープリタはもっとも内側のリストを最初に評価する。
もっとも内側のリストはクオートしてあるので、そのままもっとも内側のcdr
に
渡される。
このcdr
はリストの2番目以降の要素で構成されたリストを
もっとも外側のcdr
に渡し、それはもとのリストの3番目以降の要素で
構成されたリストを返す。
この例では、関数cdr
を繰り返し、もとのリストの先頭と2番目の要素を
除いた要素で構成されたリストを返す。
関数nthcdr
は、cdr
を繰り返し呼んで同じことを行う。
つぎの例では、引数2とリストを関数nthcdr
に渡し、
先頭と第2要素を除いたリストを得る。
つまり、リストに対してcdr
を2回繰り返したのと同じである。
(nthcdr 2 '(pine fir oak maple)) => (oak maple)
もとの4要素リストを使って、0、1、5などの数値引数を
nthcdr
に渡すとどうなるかを見てみよう。
;; リストはそのまま (nthcdr 0 '(pine fir oak maple)) => (pine fir oak maple) ;; 第1要素を除いたコピーを返す (nthcdr 1 '(pine fir oak maple)) => (fir oak maple) ;; 最初の3つの要素を除いたリストのコピーを返す (nthcdr 3 '(pine fir oak maple)) => (maple) ;; 4つの要素すべてを除いたものを返す (nthcdr 4 '(pine fir oak maple)) => nil ;; すべての要素を除いたものを返す (nthcdr 5 '(pine fir oak maple)) => nil
重要なことは、cdr
と同様にnthcdr
も
もとのリストを変更しないことである。
つまり、関数は非破壊的である。
これは、関数setcar
やsetcdr
とは大きく異なる。
setcar
名前から予想できるように、関数setcar
やsetcdr
は、
リストのcar
やcdr
に新しい値を設定する。
もとのリストを変更しないcar
やcdr
と異なり、
これらはもとのリストを変更する。
実際にその動作を試してみよう。
まず、関数setcar
から始めよう。
最初に、リストを作り、関数setq
を使って変数の値
としてそのリストを設定する。
ここでは動物のリストを作ろう。
(setq animals '(giraffe antelope tiger lion))
GNU EmacsのInfoで読んでいる場合には、いつものように 式の直後にカーソルを置いてC-x C-eとタイプすれば、この式を評価できる (筆者もこのようにして執筆している。 これは、計算環境にインタープリタがあることの1つの利点である)。
変数animals
を評価すると、リスト(giraffe antelope tiger lion)
に
束縛されていることがわかる。
animals => (giraffe antelope tiger lion)
いいかえれば、変数animals
はリスト(giraffe antelope tiger lion)
を指しているのである。
つぎに、変数animals
とクオートしたシンボルhippopotamus
を
2つの引数として関数setcar
を評価する。
それには、3要素リスト(setcar animals 'hippopotamus)
を書いて、
いつものようにそれを評価する。
(setcar animals 'hippopotamus)
この式を評価してから、変数animals
を再度評価する。
動物のリストが変化したことがわかるはずである。
animals => (hippopotamus antelope tiger lion)
リストの先頭要素がgiraffe
からhippopotamus
に変更された。
つまり、setcar
は、cons
のようにはリストに新たな要素を
追加しないことがわかる。
setcar
は、giraffe
をhippopotamus
で置き換え、
リストを変更したのである。
setcdr
関数setcdr
は関数setcar
に似ているが、
リストの先頭要素ではなく2番目以降の要素を変更する。
この動作をみるために、つぎの式を評価して変数に 家畜動物のリストを設定する。
(setq domesticated-animals '(horse cow sheep goat))
このリストを評価すると、リスト(horse cow sheep goat)
を得る。
domesticated-animals => (horse cow sheep goat)
つぎに、リストを値として持つ変数の名前とリストのcdr
に設定する
リストの2つの引数でsetcdr
を評価する。
(setcdr domesticated-animals '(cat dog))
この式を評価すると、エコー領域にリスト(cat dog)
と表示される。
これは関数が返した値である。
ここで興味があるのは「副作用」であり、変数domesticated-animals
を
評価するとわかる。
domesticated-animals => (horse cat dog)
つまり、リストは(horse cow sheep goat)
から(horse cat dog)
へと
変更された。
リストのcdr
が、(cow sheep goat)
から
(cat dog)
に変わったのである。
cons
を使った数個の式を評価して、鳥のリストを作ってみよ。
リストにそのリスト自身をcons
するとどうなるかを調べてみよ。
鳥の4要素リストの先頭要素を魚に置き換えてみよ。
さらに、そのリストの残りの部分を他の魚で置き換えてみよ。
GNU Emacsで「キル(kill)」コマンドでバッファからテキストをカットする (切り取る)と、それらはリストに保存され、 「ヤンク(yank)」コマンドで取り出せる。
(Emacsにおける単語「kill」は、実体の値を破壊しない処理を意味するが、 その用法は、歴史的な不運な偶然による。 もっと適切な用語は、キルコマンドの動作からすれば、 「clip(切り取る)」であろう。 バッファからテキストを切り取り、復元可能なように保存するからである。 Emacsのソースのすべての「kill」を「clip」に、 すべての「killed」を「clipped」に置き換えたくなる誘惑にしばしば駆られる。)
バッファからテキストを切る取ると、それはリストに保存される。 順に切り取ったテキストは順にリストに保存されるので、 リストはつぎのようになる。
("a piece of text" "last piece")
リストにテキストを追加するには、つぎのように関数cons
を使う。
(cons "another piece" '("a piece of text" "last piece"))
この式を評価すると、エコー領域に3要素リストが表示される。
("another piece" "a piece of text" "last piece")
関数car
とnthcdr
を使えば、テキストの望みの断片を取り出せる。
たとえば、つぎのコードでは、
nthcdr 1 ...
は先頭要素を除いたリストを返し、
car
はその先頭要素を返す。
つまり、もとのリストの第2要素である。
(car (nthcdr 1 '("another piece" "a piece of text" "last piece"))) => "a piece of text"
もちろん、Emacsの実際の関数はこれより複雑である。 テキストを切り取り復元するコードは、何番目の要素を指定しようとも、 Emacsがリストの望みの要素を取り出すように書く必要がある。 さらに、リストの最後に達した場合には、何も返さないのではなく、 リストの先頭要素を返すようにすべきである。
テキストの断片を保持するリストをキルリング(kill ring)と呼ぶ。
本章では、キルリングについて説明し、まず、関数zap-to-char
の動作と
その使い方を説明する。
この関数は、キルリングを操作する関数を起動する関数を使う(呼び出す)。
まずは、裾野を登ることにしよう。
本章以降では、バッファから切り取ったテキストをどのように取り出すかを説明する。 See Yanking。
zap-to-char
関数zap-to-char
は、GNU Emacsの第18版と第19版とでは書き方が異なる。
第19版での実装のほうがいくぶん簡単であるが、動作も少々異なる。
第19版の関数を説明してから、第18版の関数を説明する。
Emacs第19版の対話的関数zap-to-char
の実装では、
カーソル(つまりポイント)の位置から指定文字を含めたテキストを取りさる。
zap-to-char
で取りさったテキストはキルリングに保存され、
C-y(yank
)とタイプするとキルリングから取り出せる。
コマンドに引数を与えると、指定文字のその回数までのテキストを取りさる。
たとえば、「Thus, if the cursor were at ...
」
の先頭にカーソルがあり、文字s
を指定するとThus
を取りさる。
引数に2を与えるとcursor
のs
までを含めてThus, if the curs
を
取りさる。
Emacs第18版の実装では、ポイントから指定文字の直前までを取りさる。
したがって、上の段落の例では、s
は取りさられない。
さらに、第18版の実装では、指定文字がみつからない場合には、 バッファの最後まで行くが、第19版ではエラーになる(テキストも取りさらない)。
どれだけのテキストを取りさるかを決定するために、
どちらの版のzap-to-char
も探索関数を使う。
テキストを処理するコードでは検索は非常によく使われ、
削除コマンドと同じくらいに探索関数に注意を払うのも価値がある。
以下は、第19版での関数の実装の完全なテキストである。
(defun zap-to-char (arg char) ; version 19 implementation "Kill up to and including ARG'th occurrence of CHAR. Goes backward if ARG is negative; error if CHAR not found." (interactive "*p\ncZap to char: ") (kill-region (point) (progn (search-forward (char-to-string char) nil nil arg) (point))))
progn
function.
point
and search-forward
.
interactive
式コマンドzap-to-char
のinteractive
式はつぎのとおりである。
(interactive "*p\ncZap to char: ")
"*p\ncZap to char: "
のように二重引用符に囲まれた引数があり、
3つの部分から成る。
最初の部分はもっとも簡単でアスタリスク*
であり、
バッファが読み出し専用だった場合にエラーを発生させる。
つまり、読み出し専用バッファでzap-to-char
を使うと、
テキストを削除できずに、「Buffer is read-only(バッファは読み出し専用)」という
エラーメッセージを受け取り、端末のベルも鳴る。
"*p\ncZap to char: "
の2番目の部分はp
である。
この部分は改行\n
で終わる。
p
は、関数の第1引数には「処理した前置引数(processed prefix)」の
値を渡すことを意味する。
前置引数は、C-uに続けて数、あるいは、M-と数をタイプして渡す。
引数なしで対話的に関数を呼び出した場合には、この引数には1が渡される。
"*p\ncZap to char: "
の3番目の部分は cZap to char:
である。
この部分では、小文字のc
により、interactive
はプロンプトがあり、
引数は文字であることを期待する。
プロンプトはc
のあとに続く文字列Zap to char:
である
(コロンのうしろの空白は、見やすくするためである)。
これらにより、ユーザーに問い合わせてzap-to-char
へ渡す正しい型の引数を
準備する。
zap-to-char
の本体関数zap-to-char
の本体には、
カーソルの現在位置から指定文字を含んだテキストをキル(つまり、削除)する
コードがある。
コードの最初の部分はつぎのとおりである。
(kill-region (point) ...
(point)
はカーソルの現在位置である。
コードのつぎの部分は、progn
を使った式である。
progn
の本体は、search-forward
とpoint
の呼び出しから成る。
search-forward
を説明してからのほうが
progn
の動作を理解しやすいので、
search-forward
を説明してからprogn
を説明する。
search-forward
関数search-forward
は、zap-to-char
にて削除する指定文字を
探すために使われる。
この探索に成功すると、search-forward
は、探索文字列の最後の文字の
直後にポイントを置く
(ここでは、探索文字列は1文字である)。
逆向きに探索した場合には、探索文字列の最初の文字の直前にポイントを置く。
さらに、search-forward
は、真としてt
を返す
(したがって、ポイントの移動は「副作用」である)。
zap-to-char
では、関数search-forward
をつぎのように使う。
(search-forward (char-to-string char) nil nil arg)
関数search-forward
は4つの引数を取る。
"z"
のような文字列である必要がある。
zap-to-char
に渡される引数は単独の文字である。
コンピュータの構成方法のために、Lispインタープリタは単独の文字と
文字列を区別する。
コンピュータ内部では、単独の文字は、1文字の文字列とは異なる電気的な形式となる
(単独の文字は、コンピュータ内部では、しばしば、ちょうど1バイトで記録される。
一方、文字列は長かったり短かったりするので、コンピュータはそれに対応できる
必要がある)。
関数search-forward
は文字列を探すので、
関数zap-to-char
が引数として受け取った文字は、
コンピュータ内部では、ある形式から別の形式に変換する必要がある。
さもないと、関数search-forward
は失敗する。
関数char-to-string
を使って、この変換を行う。
nil
である。
nil
を返す。
第3引数にnil
を指定すると、探索に失敗すると関数はエラーを通知する。
search-forward
の第4引数は、繰り返し回数、
つまり、文字列の出現を何回探すかを指定する。
この引数は省略でき、繰り返し回数を指定しないと1である。
この引数が負の場合には、逆向きに探索する。
search-forward
式の概略はつぎのとおりである。
(search-forward "探索文字列" 探索範囲 探索失敗時の動作 繰り返し回数)
では、progn
を説明しよう。
progn
progn
は、個々の引数を順番に評価して最後のものの値を返す関数である。
最後以外の式は、それらの副作用のためだけに評価される。
それらが返す値は捨てられる。
progn
式の雛型はとても簡単である。
(progn 本体...)
zap-to-char
では、progn
式は2つのことを行う。
正しい位置にポイントを置くことと、
kill-region
がどこまでを削除するかがわかるようにポイントの位置を
返すことである。
progn
の第1引数はsearch-forward
である。
search-forward
は、文字列を探しあてると
検索文字列の最後の文字の直後にポイントを置く
(ここでは、検索文字列は1文字長である)。
逆向きに検索した場合は、search-forward
は検索文字列の最初の文字の
直前にポイントを置く。
ポイントの移動は副作用である。
progn
の2番目で最後の引数は、式(point)
である。
この式はポイントの値を返し、
ここでは、search-forward
が移動した位置である。
progn
式がこの値を返し、kill-region
の第2引数として
kill-region
に渡される。
zap-to-char
のまとめsearch-forward
とprogn
の動作がわかったので、
関数zap-to-char
全体としての動作を理解しよう。
kill-region
の第1引数は、コマンドzap-to-char
を与えたときの
カーソルの位置、つまり、そのときのポイントの値である。
progn
の中で、探索関数が削除する文字の直後にポイントを移動し、
point
がその位置の値を返す。
関数kill-region
は、ポイントの2つの値を組み合わせて、
最初のものをリージョンの始まり、
あとのものをリージョンの終わりと解釈してリージョンを削除する。
式search-forward
とpoint
を2つ続けて余分な2つの引数として書くと、
2つの引数を取るコマンドkill-region
は失敗するので、
関数progn
が必要なのである。
progn
式は、kill-region
に対しては1つの引数となり、
kill-region
が第2引数に必要とする1つの値を返す。
zap-to-char
の第18版での実装は第19版での実装と少々異なる。
指定文字の直前までを削除する。
また、指定文字がみつからないと、バッファの最後までを削除する。
この違いは、コマンドkill-region
の第2引数にある。
第19版の実装ではつぎのとおりであった。
(progn (search-forward (char-to-string char) nil nil arg) (point))
第18版の実装はつぎのとおりである。
(if (search-forward (char-to-string char) nil t arg) (progn (goto-char (if (> arg 0) (1- (point)) (1+ (point)))) (point)) (if (> arg 0) (point-max) (point-min)))
このコードは相当複雑に見えるが、部分ごとに見れば理解できる。
最初の部分はつぎのとおりである。
(if (search-forward (char-to-string char) nil t arg)
これはつぎのif
式に当てはめて考えられる。
(if 指定文字がみつかったなら、そこへポイントを移動する ポイントを修正し、その位置を返す さもなければ、バッファの最後に移動し、その位置を返す)
if
式の評価は、kill-region
の第2引数になる。
第1引数はポイントなので、この処理により、kill-region
は
ポイントから指定文字までのテキストを削除できるのである。
search-forward
が副作用としてポイントを移動することについては、
すでに説明した。
search-forward
が返す値は、探索に成功すればt
を、
さもなければ、search-forward
の第3引数の値に依存して
nil
かエラーメッセージを返す。
ここでは、第3引数にt
を指定しているので、
探索に失敗すると関数はnil
を返す。
これから見るように、探索でnil
が返ったときの場合を処理する
コードを書くのは簡単である。
zap-to-char
の第18版での実装では、
if
の判定条件として探索式を評価するので、探索が行われる。
探索に成功すると、Emacsはif
式の真の場合の動作を評価する。
一方、探索に失敗すると、Emacsはif
式の偽の場合の動作を評価する。
if
式では、探索に成功するとprogn
式が実行される。
すでに説明したように、関数progn
は、個々の引数を順番に評価し、
最後のものの値を返す。
それ以外の式は、それらの副作用のためだけに評価される。
それらの値は捨てられる。
zap-to-char
のこの版では、progn
式は、
関数search-forward
が文字を探しあてたときに実行される。
progn
式では2つのことを行う。
ポイントの位置を正すことと、kill-region
がどこまで削除するかを
わかるようにポイントの位置を返すことである。
progn
のコードがある理由は、search-forward
が文字列を
探しあてたあとでは、探索文字列の最後の文字の直後にポイントを置くからである
(ここでは、探索文字列は1文字長である)。
逆向きの探索では、search-forward
は探索文字列の最初の文字の
直前にポイントを置く。
しかし、関数zap-to-char
のこの版では、
指定文字を削除しないことになっている。
たとえば、zap-to-char
でz
までのテキストを削除すると、
この版ではz
は削除しない。
そのため、指定文字が削除されないようにポイントを移動する必要がある。
progn
expression
progn
式の本体progn
の本体には2つの式がある。
これを各部分がわかりやすいように輪郭を整えて注釈を加えるとつぎのようになる。
(progn (goto-char ;progn
の最初の式 (if (> arg 0) ;arg
が正ならば、 (1- (point)) ; 1文字分戻る; (1+ (point)))) ; さもなければ、1文字分進める。 (point)) ;progn
の2番目の式: ; ポイントの位置を返す。
progn
式はつぎのことを行う。
(arg
が正で)末尾へ向けた探索の場合には、Emacsは探しあてた文字列の直後に
ポイントを置く。
ポイントを1文字分戻せば、その文字を範囲から外せる。
つまり、progn
中の式は(goto-char (1- (point)))
と読める。
これは、ポイントを1文字分戻す
(関数1-
は、1+
が引数に1を加えるように、引数から1を減じる)。
一方、zap-to-char
の引数が負の場合、
探索は先頭へ向けて行われる。
if
がこの場合を検出すると、式は(goto-char (1+ (point)))
と読める
(関数1+
は引数に1を加える)。
progn
の2番目で最後の引数は、式(point)
である。
この式は、progn
の第1引数で移動したポイントの位置の値を返す。
この値はif
式の値として返され、kill-region
の第2引数として
kill-region
に渡される。
まとめると、関数はつぎのように働く。
kill-region
の第1引数は、コマンドzap-to-char
を起動したときの
カーソルの位置、つまり、そのときのポイントの値である。
続いて、探索関数は探索に成功するとポイントを移動する。
progn
式は、指定文字を削除しないようにポイントを移動し、
移動後のポイントの値を返す。
続いて、関数kill-region
がリージョンを削除する。
最後に、if
式の偽の場合の動作は、指定文字がみつからなかった場合を扱う。
関数zap-to-char
の引数が正で(あるいは指定してなくて)
指定文字がみつからない場合、
ポイントの現在位置からバッファの参照可能なリージョンの最後
(ナロイングしていなければバッファの最後)までのテキストすべてを削除する。
arg
が負で指定文字がみつからない場合、
参照可能なリージョンの先頭から削除する。
これを扱うコードは、つぎの単純なif
節である。
(if (> arg 0) (point-max) (point-min))
つまり、arg
が正の数ならばpoint-max
の値を、
さもなければpoint-min
の値を返す。
復習のために、kill-region
を起動するコードを注釈付きで示す。
(kill-region
(point) ; リージョンの始め
(if (search-forward
(char-to-string char) ; 探索文字列
nil ; 探索範囲:指定しない
t ; 失敗したらnil
を返す
arg) ; 繰り返し回数
(progn ; 真の場合の動作
(goto-char
(if (> arg 0)
(1- (point))
(1+ (point))))
(point))
(if (> arg 0) ; 偽の場合の動作
(point-max)
(point-min))))
以上でわかるように、第19版の実装は、第18版の実装より少々小さく、 より簡単である。
kill-region
関数zap-to-char
は関数kill-region
を用いる。
この関数はとても簡単であり、説明文字列の一部を省略するとつぎのとおりである。
(defun kill-region (beg end) "Kill between point and mark. The text is deleted but saved in the kill ring." (interactive "*r") (copy-region-as-kill beg end) (delete-region beg end))
重要な点は、つぎの節で説明する関数delete-region
と
copy-region-as-kill
を使っていることである。
delete-region
:Cへ回り道コマンドzap-to-char
は関数kill-region
を使い、
それはさらにcopy-region-as-kill
とdelete-region
と
いう2つの関数を使っている。
関数copy-region-as-kill
はつぎの節で説明するが、
リージョンのコピーをキルリングに保存して取り出せるようにする
(See copy-region-as-kill)。
関数delete-region
はリージョンの内容を削除するが、
その内容を戻すことはできない。
これまでに説明したコードと異なり、delete-region
はEmacs Lispで
書かかれていない。
Cで書かかれており、GNU Emacsシステムの基本操作関数の1つである。
とても簡単なので、Lispから回り道して、ここで説明することにする。
Emacsのほとんどの基本操作関数と同様に、delete-region
は、
Cのマクロ、コードの雛型となるマクロを用いて書かれている。
マクロの最初の部分はつぎのとおりである。
DEFUN ("delete-region", Fdelete_region, Sdelete_region, 2, 2, "r", "Delete the text between point and mark.\n\ When called from a program, expects two arguments,\n\ character numbers specifying the stretch to be deleted.")
マクロの書き換え処理の詳細を説明するつもりはないが、
このマクロは単語DEFUN
で始まることを指摘しておく。
Lispのdefun
と同じ目的を果たすコードなので、単語DEFUN
が選ばれた。
単語DEFUN
に続けて括弧の中には7つの部分がある。
delete-region
である。
Fdelete_region
である。
慣習的にF
で始める。
Cでは名前としてハイフンを使えないので、かわりに下線を使う。
F
のかわりにS
で始める。
interactive
の引数と同じであり、
文字に続けてプロンプトがあってもよい。
ここでは、文字は"r"
であり、関数への2つの引数はバッファの
リージョンの先頭と最後であることを示す。
この例では、プロンプトはない。
\n
に続けてバックスラッシュと改行で書く必要がある。
続いて、オブジェクトの種類を指定する文とともに仮引数があり、
さらに、マクロの「本体」とも呼ぶべきものが続く。
delete-region
の本体はつぎの3行から成る。
validate_region (&b, &e); del_range (XINT (b), XINT (e)); return Qnil;
最初の関数validate_region
では、
リージョンの始めと終わりとして渡された値が
正しい型で正しい範囲にあるかどうかを調べる。
2番目の関数del_range
で、実際にテキストを削除する。
この関数の処理が正常に終了すると、
これを表すために3番目の行でQnil
を返す。
del_range
は複雑な関数なので、その中身は調べないことにする。
バッファを更新し、その他のことも行う。
しかし、del_range
に渡される2つの引数を調べることは価値がある。
これらは、XINT (b)
とXINT (e)
である。
言語Cに関する限り、b
とe
は、削除すべきバッファの先頭と最後を
表す2つの32ビット整数である。
しかし、Emacs Lispの他の数と同様に、32ビットの内の24ビットのみを使っている。
残りの8ビットは、情報の型の記録やその他の目的に利用される
(ある種のマシンでは、6ビットのみを使う)。
ここでは、これらの数がバッファの位置を表すことを示すために8ビットを使う。
数の中のビットをこのように使うことをタグ(tag)と呼ぶ。
各32ビット整数で8ビットタグを利用すると、タグを利用しない場合に比べて
高速に動作するようにEmacsを書くことができる。
一方で、数は24ビットの範囲に制限されるため、Emacsのバッファは
約8メガバイトに制限される
(コンパイルするまえに、ファイルemacs/src/config.h
に
VALBITS
とGCTYPEBITS
の定義を追加すれば、
バッファの最大サイズを増加できる。
Emacsディストリビューションに含まれるファイル
emacs/etc/FAQ
の注意書きを参照してほしい)。
XINT
は、32ビット長のLispオブジェクトから24ビット整数を取り出す
Cのマクロである。
他の目的に用いる8ビットは捨てられる。
したがって、del_range (XINT (b), XINT (e))
は、
b
で始まりe
で終わるリージョンを削除する。
Lispを書く人の観点からは、Emacsは非常に簡単であるが、 その背後には、すべてが正しく働くように、とても複雑なことが隠れている。
defvar
による変数の初期化関数delete-region
と異なり、関数copy-region-as-kill
は
Emacs Lispで書かれている。
バッファのリージョンのコピーを変数kill-ring
に保存する。
本節では、この変数の作成と初期化の方法を説明する。
(kill-ring
はふさわしくない名称であることを再度指摘しておく。
バッファから切り取ったテキストは戻すことができる。
キルリングは死体のリングではなく、復活できるテキストのリングである。)
Emacs Lispでは、kill-ring
のような変数は、
スペシャルフォームdefvar
を用いて作成し初期化する。
この名称は「define variable(変数を定義する)」からきている。
スペシャルフォームdefvar
は、変数の値を設定するという意味では
setq
に似ている。
しかし、setq
とは2つの点で異なる。
まず、値を持っていない変数にのみ値を設定することである。
変数にすでに値があれば、defvar
は既存の値を書き換えない。
第二に、defvar
は説明文字列を有することである。
任意の変数の現在の値は、関数describe-variable
を使って調べることができ、
普通、C-h vとタイプすれば起動できる。
C-h vとタイプして問い合わせにkill-ring
(に続けて<RET>)と
タイプすれば、今のキルリングに何が入っているかがわかるが、とても多量であろう。
一方、本書を読む以外の操作をEmacsで行っていなければ、キルリングには
何もないであろう。
バッファ*Help*
の最後には、つぎのようなkill-ring
の
説明文字列があるはずである。
Documentation: List of killed text sequences.
キルリングはつぎのようにdefvar
で定義してある。
(defvar kill-ring nil "List of killed text sequences.")
この変数定義では、変数に初期値nil
を設定している。
何も保存していないときには、コマンドyank
で何も戻ってほしくないので、
この値には意味がある。
説明文字列は、defun
の説明文字列と同じである。
apropos
のようなある種のコマンドは説明文の最初の1行しか表示しないので、
defun
の説明文字列と同様に説明文の最初の行は完全な文にしておく。
また、C-h v(describe-variable
)で表示したときに
変にならないように、続く行は字下げしない。
ほとんどの変数はEmacsの内部用であるが、
コマンドedit-options
で設定するオプションであるものもある
(これらの設定は、編集作業中でのみ有効である
恒久的に設定するには、ファイル.emacs
を書く。
See Emacs Initialization)。
Emacsでは、設定可能な変数とそれ以外のものとは、
説明文字列の先頭のアスタリスク*
で区別する。
たとえば、
(defvar line-number-mode nil "*Non-nil means display line number in mode line.")
line-number-mode
の値は、コマンドedit-options
を使って
変更できることを意味する。
もちろん、line-number-mode
の値は、
つぎのようにsetq
式の内側に書いて評価しても変更できる。
(setq line-number-mode t)
See Using setq。
copy-region-as-kill
関数copy-region-as-kill
は、バッファからリージョンのテキストをコピーし
変数kill-ring
に保存する。
コマンドkill-region
の直後にcopy-region-as-kill
を呼ぶと、
Emacsは、新たにコピーしたテキストを直前にコピーしたテキストに追加する。
つまり、そのテキストを復元すると、そのテキストと直前のテキストを
まとめて得ることになる。
一方で、copy-region-as-kill
のまえに別のコマンドを実行した場合には、
この関数はテキストを独立した項目としてキルリングに保存する。
わかりやすいように整形して注釈を付加した第18版の
copy-region-as-kill
の完全なテキストをつぎに示す。
(defun copy-region-as-kill (beg end) "Save the region as if killed, but don't kill it." (interactive "r") (if (eq last-command 'kill-region) ;; 真の場合の動作:新たにコピーしたテキストを ;; 直前にコピーしたテキストに追加する。 (kill-append (buffer-substring beg end) (< end beg)) ;; 偽の場合の動作:新たにコピーしたテキストを ;; 独立したものとしてキルリングに追加し、 ;; 必要ならばキルリングを短くする。 (setq kill-ring (cons (buffer-substring beg end) kill-ring)) (if (> (length kill-ring) kill-ring-max) (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil))) (setq this-command 'kill-region) (setq kill-ring-yank-pointer kill-ring))
この関数も部分部分に分解できる。
(defun copy-region-as-kill (引数リスト) "説明文..." (interactive "r") 本体...)
引数はbeg
とend
であり、
"r"
が指定された対話的関数であるので、
2つの引数はリージョンの始まりと終わりを参照する必要がある。
本書を始めから読んでいる読者ならば、関数のこの部分を理解するのは簡単であろう。
単語「kill」が通常の意味と異なることを思い出せば、 説明文の内容に混乱することもないであろう。
関数の本体はif
節で始まる。
この節で、2つの異なる場合を分ける。
コマンドkill-region
の直後に、このコマンドが実行されたかどうかを区別する。
そうならば、直前にコピーしたテキストに新たなリージョンを追加する。
さもなければ、直前の断片とは独立したテキストの断片として
キルリングの先頭に挿入する。
関数の最後の2行は、2つのsetq
式である。
1つは、変数this-command
にkill-region
を設定し、
もう1つでは、変数kill-ring-yank-pointer
がキルリングを指すようにする。
copy-region-as-kill
の本体は詳しく説明する価値がある。
copy-region-as-kill
copy-region-as-kill
の本体関数copy-region-as-kill
は、
連続してキルしたテキストは1つの断片にまとめるように書かれている。
キルリングからテキストを取り出すと、1つの断片として得ることになる。
さらに、カーソルの現在位置から終わりに向けたキルでは
直前にコピーしたテキストの末尾に追加し、
先頭向けのコピーでは直前にコピーしたテキストの先頭に追加する。
このようにして、テキスト内の語順は正しく保たれる。
この関数では、現在と直前のEmacsコマンドを記録する2つの変数を用いる。
this-command
とlast-command
である。
通常、関数が実行されると、Emacsは、this-command
の値に
実行する関数(ここでは、copy-region-as-kill
)を設定する。
同時に、this-command
の直前の値をlast-command
に設定する。
しかし、コマンドcopy-region-as-kill
では違っていて、
this-command
の値にはkill-region
を設定する。
これは、copy-region-as-kill
を呼んだ関数の名前である。
関数copy-region-as-kill
の本体の始めの部分では、
last-command
の値がkill-region
かどうかを
if
式で調べている。
そうならば、if
式の真の場合の動作が評価される。
そこでは、関数kill-append
を使って、
この呼び出しでコピーするテキストをキルリングの先頭要素(CAR)に連結する。
一方、last-command
の値がkill-region
でなければ、
関数copy-region-as-kill
は新たな要素をキルリングに追加する。
まだ説明していない関数eq
を用いているが、
if
式はつぎのように読める。
(if (eq last-command 'kill-region) ;; 真の場合の動作 (kill-append (buffer-substring beg end) (< end beg))
関数eq
は、第1引数が第2引数と同じLispオブジェクトかどうかを検査する。
関数eq
は、等しいかどうかを検査する関数equal
に似ているが、
2つの表現がコンピュータ内部で実際に同じオブジェクトかどうかを検査する。
equal
は、2つの式の構造と内容が同じかどうかを検査する。
kill-append
function
copy-region-as-kill
kill-append
関数kill-append
はつぎのとおりである。
(defun kill-append (string before-p) (setcar kill-ring (if before-p (concat string (car kill-ring)) (concat (car kill-ring) string))))
この関数も部分に分けて説明できる。
関数setcar
は、キルリングのCARに新たなテキストを連結するために
concat
を使っている。
テキストを先頭に挿入するのか末尾に追加するのかは、
if
式の結果に依存する。
(if before-p ; 判定条件 (concat string (car kill-ring)) ; 真の場合の動作 (concat (car kill-ring) string)) ; 偽の場合の動作
直前のコマンドでキルしたリージョンの直前のリージョンをキルしたときには、
直前のキルで保存したテキストの先頭に挿入するべきである。
逆に、直前にキルしたリージョンの直後に続くテキストをキルした場合には、
直前のテキストの末尾に追加するべきである。
if
式では、新たに保存するテキストを直前に保存したテキストの先頭に
挿入するか末尾に追加するかを述語before-p
を用いて決めている。
シンボルbefore-p
は、kill-append
の引数の名前の1つである。
関数kill-append
が評価されると、実引数を評価した結果の値に束縛される。
ここでは、式(< end beg)
である。
この式では、このコマンドでキルしたテキストが直前のコマンドでキルしたテキストの
まえにあるか、うしろにあるかを直接には決定しない。
変数end
の値が変数beg
の値より小さいかどうかのみを決定する。
そうであった場合には、バッファの先頭に向けてである可能性が高い。
すると、述語式(< end beg)
の評価結果は真となり、
テキストは直前のテキストの先頭に挿入される。
一方、変数end
の値が変数beg
の値より大きければ、
テキストは直前のテキストの末尾に追加される。
新たに保存するテキストを先頭に挿入するときには、 既存のテキストのまえに新たなテキストを連結する。
(concat string (car kill-ring))
テキストを追加する場合には、既存のテキストのうしろに連結する。
(concat (car kill-ring) string))
この動作を理解するには、まず、関数concat
を復習しておく必要がある。
関数concat
は、2つの文字列を繋げる。
結果も文字列である。
たとえば、
(concat "abc" "def") => "abcdef" (concat "new " (car '("first element" "second element"))) => "new first element" (concat (car '("first element" "second element")) " modified") => "first element modified"
これでkill-append
の動作を理解でき、キルリングの内容を変更することが
わかる。
キルリングはリストであり、各要素は保存したテキストである。
関数setcar
は、実際には、このリストの先頭要素を変更する。
それにはconcat
を用いて、
キルリングのもとの先頭要素(キルリングのCAR)を
もとの保存されたテキストと新たに保存したテキストを連結したもので置き換える。
新たに保存するテキストは、バッファからそれを切り取った位置に依存して、
もとのテキストの先頭か末尾に追加される。
連結したものがキルリングの新たな先頭要素になる。
たとえば、筆者のキルリングの始めの部分はつぎのようになっている。
("concatenating together" "saved text" "element" ...
copy-region-as-kill
の偽の場合の動作さて、copy-region-as-kill
の説明に戻ろう。
直前のコマンドがkill-region
でない場合には、
kill-append
を呼ぶかわりに、つぎに示した偽の場合の動作を呼び出す。
(if 判定条件 真の場合の動作 ;; 偽の場合の動作 (setq kill-ring (cons (buffer-substring beg end) kill-ring)) (if (> (length kill-ring) kill-ring-max) (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))
偽の場合の動作のsetq
の行では、キルした文字列をもとのキルリングに
追加した結果をキルリングの新たな値に設定する。
つぎの例からこの動作を理解できる。
(setq example-list '("here is a clause" "another clause"))
C-x C-eでこの式を評価してから、example-list
を評価すると
つぎのような結果になる。
example-list => ("here is a clause" "another clause")
このリストに新たな要素を追加するには、つぎの式を評価すればよい。
(setq example-list (cons "a third clause" example-list))
example-list
を評価すると、その値はつぎのとおりである。
example-list => ("a third clause" "here is a clause" "another clause")
つまり、cons
で「the third clause」をリストに追加したのである。
以上は、関数内でsetq
とcons
とが行うことと同じであるが、
buffer-substring
を使ってテキストのリージョンのコピーを作り出して
cons
に渡している。
その行を改めてつぎに記しておく。
(setq kill-ring (cons (buffer-substring beg end) kill-ring))
copy-region-as-kill
の偽の場合の動作のつぎの部分もif
節である。
このif
節は、キルリングが長くなり過ぎるのを防ぐ。
つぎのようになっている。
(if (> (length kill-ring) kill-ring-max) (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))
このコードでは、キルリングの長さが許された最大長よりも
大きいかどうかを検査する。
最大長はkill-ring-max
の値である(デフォルトは30)。
キルリングの長さが長すぎる場合には、キルリングの最後の要素をnil
に
設定する。
これには、2つの関数nthcdr
とsetcdr
を使う。
setcdr
についてはすでに説明した(see setcdr)。
setcar
がリストのCARを設定するように、
setcdr
はリストのCDRを設定する。
しかし、ここではsetcdr
はキルリング全体のcdr
を設定するのではない。
関数nthcdr
が使われていて、キルリングの最後の要素の直前のcdr
を
設定するのである。
つまり、最後の要素の直前のcdr
はキルリングの最後の要素であるから、
キルリングの最後の要素を設定することになる。
関数nthcdr
は、リストのCDRを繰り返し取るように動作する。
つまり、CDRのCDRのCDRの...のCDRを取る。
N回繰り返した結果を返す。
したがって、たとえば、4要素リストを3要素リストにするには、
最後の要素の直前のCDRをnil
にしてリストを短くすればよい。
つぎの3つの式を順に評価すれば、これを理解できるであろう。
まず、trees
の値として(maple oak pine birch)
を設定する。
つぎに、2つめのCDRのCDRをnil
にしてから、
trees
の値を見てみる。
(setq trees '(maple oak pine birch)) => (maple oak pine birch) (setcdr (nthcdr 2 trees) nil) => nil trees => (maple oak pine)
(setcdr
式が返す値は、CDRをnil
に設定したので、
nil
である。)
copy-region-as-kill
では、関数nthcdr
は、
キルリングの許された最大長引く1回だけCDRを取り、
その要素(キルリングの残りの要素)のCDRにnil
を設定する。
これにより、キルリングが長くなり過ぎるのを防ぐ。
関数copy-region-as-kill
の最後の行の直前はつぎのとおりである。
(setq this-command 'kill-region)
この行は、if
式の内側でも外側でもなく、
copy-region-as-kill
が呼ばれるごとに評価される。
ここが、this-command
にkill-region
を設定する場所である。
すでに説明したように、つぎにコマンドが与えられると、
変数last-command
にはこの値が設定される。
関数copy-region-as-kill
の最後の行は、つぎのとおりである。
(setq kill-ring-yank-pointer kill-ring)
kill-ring-yank-pointer
はグローバル変数であり、
kill-ring
と同じに設定される。
kill-ring-yank-pointer
はポインタと名前が付いているが、
キルリングと同じ変数である。
しかし、変数の使われ方が人間にわかりやすいように、この名称が選ばれた。
この変数は、yank
やyank-pop
などの関数で使われる
(see Yanking)。
これで、バッファから切り取ったテキストを復元する ヤンクコマンドのコードの説明に進める。 しかし、ヤンクコマンドを説明するまえに、コンピュータでのリストの実装方法を 学んでおくのがよいであろう。 そうすれば、「ポインタ」などの用語の不可解さが明らかになる。
これまでに説明した関数のいくつかを以下にまとめておく。
car
cdr
car
はリストの先頭要素を返す。
cdr
はリストの2番目以降の要素を返す。
たとえば、
(car '(1 2 3 4 5 6 7)) => 1 (cdr '(1 2 3 4 5 6 7)) => (2 3 4 5 6 7)
cons
cons
は、第1引数を第2引数のまえに置いたリストを作る。
たとえば、
(cons 1 '(2 3 4)) => (1 2 3 4)
nthcdr
cdr
を「n」回適用した結果を返す。
たとえば、
(nthcdr 3 '(1 2 3 4 5 6 7)) => (4 5 6 7)
setcar
setcdr
setcar
はリストの先頭要素を変更する。
setcdr
はリストの2番目以降の要素を変更する。
たとえば、
(setq triple '(1 2 3)) (setcar triple '37) triple => (37 2 3) (setcdr triple '("foo" "bar")) triple => (37 "foo" "bar")
progn
たとえば、
(progn 1 2 3 4) => 4
save-restriction
search-forward
4つの引数を取る。
nil
を返すかエラーメッセージを返すか指定する。
省略できる。
kill-region
delete-region
copy-region-as-kill
kill-region
は、ポイントとマークのあいだのテキストを
バッファから切り取り、ヤンクで復元できるように、
キルリングにそのテキストを保存する。
delete-region
は、ポイントとマークのあいだのテキストを
バッファから取りさり、破棄する。
復元することはできない。
copy-region-as-kill
はポイントとマークのあいだのテキストを
キルリングにコピーし、キルリングからそのテキストを復元できるようにする。
この関数は、バッファからテキストを取りさったりしない。
search-forward
を使わないこと。
さもないと、Emacsの既存のsearch-forward
を書き換えてしまう。
かわりに、test-search
のような名前を使う)。
copy-region-as-kill
はthis-command
を設定しない。
この変更の帰結はなんであろう?
その意図は何であろう?
Lispでは、アトムは単純な方法で記録されている。
現実の実装が単純ではないとしても、理論的には単純である。
たとえば、アトムrose
は、r
、o
、s
、
e
の4つの連続した文字として記録されている。
一方で、リストは異なった方法で記録されている。
その機構は同様に単純であるが、その考え方に慣れるには時間がかかる。
リストは一連のポインタ対を用いて記録されている。
ポインタ対の最初のポインタはアトムやリストを指し、
ポインタ対の2番目のポインタはつぎの対やシンボル、
あるいは、リストの終わりを表すnil
を指す。
ポインタ自身は、とても単純で、それが指すもののアドレスである。 したがって、リストは一連のアドレス対として記録される。
たとえば、リスト(rose violet buttercup)
には3つの要素、
rose
、violet
、buttercup
がある。
コンピュータ内部では、rose
のアドレスは、
アトムviolet
の場所を示すアドレスを与えるアドレスとともに
メモリに記録されている。
(violet
の場所を示す)アドレスは、
アトムbuttercup
の場所を示すアドレスを与えるアドレスとともに
記録されている。
複雑に聞こえるであろうが、図で表せば簡単である。
図では、各箱は、メモリアドレス形式のLispオブジェクトを保持する
コンピュータのメモリを表す。
箱、つまり、アドレスは対になっている。
各矢印は、アトムや他のアドレスの対のアドレスで表されるものを指す。
最初の箱はrose
のアドレスであり、矢印はrose
を指す。
2番目の箱は、つぎの箱の対のアドレスであり、
その先頭部分はviolet
のアドレスであり、
2番目の部分はつぎの対のアドレスである。
最後の箱はシンボルnil
を指し、リストの終わりを表す。
setq
などの関数で変数にリストを設定すると、
変数には最初の箱のアドレスを設定する。
つぎの式
(setq bouquet '(rose violet buttercup))
を評価するとつぎのような状況になる。
この場合、シンボルbouquet
は、最初の箱の対のアドレスを保持する。
もちろん、シンボルbouquet
もアドレスを保持する箱の組で構成され、
その1つには表示名bouquet
のアドレスが入り、
2番目にはシンボルに結び付けられた関数定義のアドレスが入り、
3番目にはリスト(rose violet buttercup)
の最初の箱の対のアドレスが入り、
のように続く。
同じリストを異なる箱表記方法で示すこともできる。
始めの節では、シンボルを箪笥と考えればよいと述べた。 関数定義はある引き出しに入れてあり、値は別の引き出しに入れてあるのである。 関数定義を収めた引き出しの中身を変えることなく、 値を収めた引き出しの中身を変更でき、その逆もそうである。 実際、各引き出しに収められているのは、値や関数定義のアドレスである。 屋根裏部屋でみつけた古い箪笥の引き出しから、宝物を埋めた場所を記した 地図をみつけるようなものである。
(シンボルには、名前、関数定義、値に加えて、その他の情報を記録するための 属性リスト(property list)を収める引き出しもある。 ここでは属性リストについては 説明しないので、Property Listsを参照。)
仮想的な表現をつぎに示す。
シンボルにリストのCDRを設定しても、リスト自体は変わらない。 シンボルはリストを辿ったアドレスを持つだけである (専門用語では、CARやCDRは「非破壊的」である)。 したがって、つぎの式を評価すると
(setq flowers (cdr bouquet))
つぎのようになる。
flowers
の値は(violet buttercup)
であり、
つまり、シンボルflowers
は、violet
のアドレスを最初の箱に、
buttercup
のアドレスを2番目の箱に持つような箱の対のアドレスを保持する。
アドレスを収めた箱の対をコンスセル(cons cell)とか ドットペアー(dotted pair)と呼ぶ。 コンスセルやドットペアーについて 詳しくは、Dotted Pair Notationや See List Type。
関数cons
は、上に示した一連のアドレスのまえにアドレスの新たな対を加える。
たとえば、つぎの式
(setq bouquet (cons 'lilly bouquet))
を評価すると、つぎのようになる。
しかし、つぎの式を評価するとわかるように、
これによりシンボルflowers
の値が変更されることはない。
(eq (cdr (cdr bouquet)) flowers)
は、真を表すt
を返す。
再設定しない限り、flowers
の値は(violet buttercup)
である。
つまり、flowers
は、最初の部分にviolet
のアドレスを
収めたコンスセルのアドレスを持つ。
さらに、すでに存在するいかなるコンスセルも変更しない。
それらはそのまま存続する。
したがって、LispでリストのCDRを取り出すと、
一連のコンスセルの中のつぎのコンスセルのアドレスを得るのである。
リストのCARを取り出すと、リストの先頭要素のアドレスを得る。
新たな要素をリストにcons
すると、
リストのまえに新たなコンスセルを置くのである。
以上がすべてである。
Lispの根底にある構造は、とても単純なのである。
一連のコンスセルの最後のアドレスは何を指すのであろう?
それは空リスト、つまり、nil
のアドレスである。
まとめると、Lispの変数に値を設定すると、 変数が参照するリストのアドレスが設定される。
flowers
にviolet
とbuttercup
を設定せよ。
このリストにさらに2つの花をコンスし、
この新たなリストをmore-flowers
に設定せよ。
flowers
のCARに魚を設定せよ。
このとき、more-flowers
はどんなリストを持つか?
GNU Emacsで「キル」コマンドでバッファからテキストを切り取ったときには、 「ヤンク」コマンドでそのテキストを取り出せる。 バッファから切り取ったテキストはキルリングに保存され、 ヤンクコマンドはキルリングの適当な内容をバッファに挿入する (同じバッファである必要はない)。
簡単なコマンドC-y(yank
)では、キルリングの先頭要素を
カレントバッファに挿入する。
C-yの直後にM-yを続けると、先頭要素を2番目の要素で置き換える。
さらにM-yを続けると、2番目の要素を3番目の要素で、4番目の要素で、
5番目の要素で置き換えるというようになる。
キルリングの最後の要素に達した場合には、一巡して先頭要素で置き換える
(これが、キルリングを「リスト」とは呼ばずに「リング」と呼ぶ理由である。
しかし、テキストを保持する実際のデータ構造はリストである。
リストをリングとして扱う方法の詳細は、See Kill Ring)。
kill-ring-yank-pointer
variable.
キルリングは、文字列のリストである。 これはつぎのようになっている。
("some text" "a different piece of text" "yet more text")
これが筆者のキルリングの内容であったとすると、
C-yとタイプするとカレントバッファのカーソル位置に文字列some text
が
挿入される。
コマンドyank
は、コピーしてテキストを複製するためにも使える。
コピーしたテキストはバッファからは切り取られずに、
そのコピーがキルリングに置かれ、ヤンクすると挿入できる。
キルリングからテキストを取り出すには3つの関数が使われる。
C-yにバインドされたyank
、
M-yにバインドされた yank-pop
、
これら2つの関数が使うrotate-yank-pointer
である。
これらの関数は、変数kill-ring-yank-pointer
を介してキルリングを参照する。
関数yank
とyank-pop
の挿入を行うコードはつぎのとおりである。
(insert (car kill-ring-yank-pointer))
yank
とyank-pop
がどのように動作するかを理解するには、
まず、変数kill-ring-yank-pointer
と関数rotate-yank-pointer
を
説明しておく必要がある。
kill-ring-yank-pointer
kill-ring-yank-pointer
は、変数kill-ring
と同じように、変数である。
他のLisp変数のように、それが指す値に束縛することで何かを指している。
したがって、キルリングの値がつぎのとおりであり、
("some text" "a different piece of text" "yet more text")
また、kill-ring-yank-pointer
が2番目の語句を指していれば、
kill-ring-yank-pointer
の値はつぎのようになる。
("a different piece of text" "yet more text")
前章で説明したように(see List Implementation)、
kill-ring
とkill-ring-yank-pointer
とが指すテキストのコピーは、
別々にコンピュータが保持するのではない。
「a different piece of text」や「yet more text」の語句は、複製されない。
かわりに、2つのLisp変数はテキストの同じ断片を指す。
図示するとつぎのようになる。
変数kill-ring
と変数kill-ring-yank-pointer
の
どちらもポインタである。
しかし、キルリング自体は、実際にその要素から構成されている。
kill-ring
は、リストを指すというよりは、リストそのもののようにいう。
逆に、kill-ring-yank-pointer
はリストを指すというようにいう。
同じものを2つの異なる方式で呼ぶのは、最初は、混乱のもとであるが、
熟考すると意味のあることがわかる。
キルリングは、Emacsバッファから切り取った情報を保持する完全なデータ構造を
一般的に意味する。
一方で、kill-ring-yank-pointer
は、挿入される先頭要素(CAR)となる
キルリングの部分を指すのである。
関数rotate-yank-pointer
は、
kill-ring-yank-pointer
が指すキルリングの要素を変更する。
キルリングの最後の要素のつぎを指すようなときには、
自動的にキルリングの先頭要素を指すようにする。
このようにしてリストをリングに変換している。
関数rotate-yank-pointer
自体のコードは難しくはないが、
こまごましたことを含む。
この関数と、さらに簡単な関数yank
とyank-pop
は、付録で説明する。
See Kill Ring。
yank
とnthcdr
の演習問題describe-variable
)を使って、読者のキルリングの内容を
調べてみよ。
キルリングにいくつか項目を追加してから、その値を再度調べてみよ。
M-y(yank-pop
)を使って、キルリング全体を調べてみよ。
読者のキルリングには項目はいくつあるか?
kill-ring-max
の値を調べてみよ。
読者のキルリングは満杯になっているか、あるいは、
テキスト断片をさらに保存できるかどうかを調べてみよ。
nthcdr
とcar
を使って、リストの先頭要素、第2要素、第3要素、
第4要素を返す一連の式を作ってみよ。
Emacs Lispには、式や一連の式を繰り返し評価する主要な方法が2つあり、
while
ループを使う方法と再帰(recursion)を使う方法である。
繰り返しはとても重要である。 たとえば、4つの文だけ先へ進むには、 1つの文のみ先へ進むだけのプログラムを書き、それを4回繰り返せばよい。 人間は繰り返し回数をまちがえたり処理を誤ったりするが、 コンピュータが飽きたり疲れたりすることはないので、 繰り返し動作が有害な結果を生むことはない。
while
スペシャルフォームwhile
は、第1引数を評価した結果が真か偽かを検査する。
これは、Lispインタープリタがif
に対して行うことに似ているが、
その検査後にインタープリタが行うことは異なる。
while
式では、第1引数を評価した結果が偽ならば、
Lispインタープリタは式の残りの部分(式の本体)を飛び越し、
それらを評価しない。
しかし、値が真ならば、Lispインタープリタは式の本体を評価し、
再度、第1引数が真か偽かを検査する。
第1引数を再度評価した結果が真ならば、
Lispインタープリタは式の本体を再度評価する。
while
式の雛型はつぎのとおりである。
(while 判定条件 本体...)
評価したときにwhile
式の判定条件が真を返す限り、
本体を繰り返し評価する。
飛行機が旋回(ループ)するように、Lispインタープリタが同じことを
何度も何度も繰り返すので、この処理をループと呼ぶ。
判定条件の評価結果が偽ならば、Lispインタープリタはwhile
式の
残りを評価せず「ループから出る」。
明らかに、while
の第1引数の評価結果がつねに真ならば、
それに続く本体は何度も何度も...何度も...永久に評価される。
逆に、評価結果がけっして真にならなければ、本体の式はけっして評価されない。
while
ループを書く工程は、続く式を評価したい回数だけ真を返し、
そのあとは偽を返すような判定条件を選ぶことである。
while
を評価した結果返される値は、判定条件の値である。
この帰結として興味深いことは、エラーなしに評価されるwhile
ループは、
1回繰り返そうが100回繰り返そうがまったく繰り返さなくても、
nil
、つまり、偽を返すことである。
正しく評価できたwhile
式は、けっして真を返さない!
つまり、while
はつねに副作用のために評価されるのであり、
while
ループの本体の式を評価した効果だけのためである。
これは意味のあることである。
ほしいのは単なる繰り返し動作ではなく、
ループの式を繰り返し評価したときの効果がほしいのである。
while
loop that uses a list.
while
, car
, cdr
.
while
ループとリストwhile
ループを制御する一般的な方法は、
リストに要素があるかどうかを検査することである。
要素があればループを繰り返すが、要素がなければ繰り返しを終える。
これは重要な技法なので、例示のために短い例を作ることにする。
リストに要素があるかどうかを検査する簡単な方法は、
リストを評価することである。
要素がなければ、空リストであるから空リスト()
が返され、
これは偽を意味するnil
の同義語である。
一方、リストに要素があれば、評価するとこれらの要素を返す。
Lispインタープリタは、nil
以外の値を真と解釈するので、
要素を返すリストはwhile
ループの検査では真になる。
たとえば、つぎのsetq
式を評価すれば、
変数empty-list
にnil
を設定できる。
(setq empty-list ())
setq
式を評価しておけば、いつものようにシンボルの直後に
カーソルを置いてC-x C-eとタイプすれば変数empty-list
を
評価できる。
エコー領域にはnil
と表示される。
empty-list
一方、つぎの2つの式を評価するとわかるように、 要素を持つリストを変数に設定して、その変数を評価するとリストが表示される。
(setq animals '(giraffe gazelle lion tiger)) animals
したがって、リストanimals
に要素があるかどうかを検査する
while
ループを書くと、ループの始めの部分はつぎのようになる。
(while animals ...
while
が第1引数を検査するとき、変数animals
が評価される。
これはリストを返す。
リストに要素がある限り、while
は検査結果は真であると解釈する。
しかし、リストが空になると検査結果は偽であると解釈する。
while
ループが永久に廻り続けるのを防ぐには、
最終的にリストが空になるような機構を与える必要がある。
しばしば使われる技法は、while
式の中の式の1つで、
リストの値にリストのCDRを設定することである。
関数cdr
を評価するたびに、リストは短くなり、最終的には空リストになる。
その時点でwhile
ループの検査は偽を返し、
while
の引数はそれ以上評価されなくなる。
たとえば、つぎの式で、動物のリストを束縛した変数animals
に
もとのリストのCDRを設定できる。
(setq animals (cdr animals))
まえの式を評価してからこの式を評価すると、
エコー領域に(gazelle lion tiger)
と表示される。
この式を再度評価すると、エコー領域に(lion tiger)
と表示される。
さらに評価すると(tiger)
となり、
さらに評価すると空リストになりnil
と表示される。
関数cdr
を繰り返し使って最終的に判定条件が偽になるような
while
ループの雛型はつぎのようになる。
(while リストが空かどうか検査 本体... リストのcdrをリストに設定)
この検査とcdr
の利用は、
リスト全体を調べてリストの各要素を1行に表示する関数で使える。
print-elements-of-list
関数print-elements-of-list
はリストを用いたwhile
ループの例である。
この関数は、複数行出力する。
エコー領域は1行分のみなので、これまでの関数の動作例を示すようには
Infoの中で評価して動作を示すことができない。
かわりに、式をバッファ*scratch*
にコピーしてから、
そのバッファで評価する必要がある。
式をコピーするには、
リージョンの始めをC-<SPC>(set-mark-command
)で
マークし、カーソルをリージョンの終わりに移動してから、
M-w(copy-region-as-kill
)を使ってリージョンをコピーする。
つぎに、バッファ*scratch*
にて、
C-y(yank
)とタイプすればその式を取り出せる。
バッファ*scratch*
に式をコピーしてから、各式を順番に評価する。
最後の式(print-elements-of-list animals)
は、必ず、
C-u C-x C-eとタイプして、つまり、
eval-last-sexp
に引数を与えて評価すること。
これにより、評価結果は、エコー領域ではなく、
バッファ*scratch*
に表示される
(さもないと、エコー領域には、^Jgiraffe^J^Jgazelle^J^Jlion^J^Jtiger^Jnil
のように表示される。
ここで、^J
は改行のことであり、
バッファ*scratch*
で1行に1語ずつ表示する。
これらの式をInfoバッファで評価すれば、この効果を見ることができる)。
(setq animals '(giraffe gazelle lion tiger)) (defun print-elements-of-list (list) "Print each element of LIST on a line of its own." (while list (print (car list)) (setq list (cdr list)))) (print-elements-of-list animals)
バッファ*scratch*
で3つの式を順番に評価すると、
バッファにはつぎのように表示される。
giraffe gazelle lion tiger nil
リストの各要素が(関数print
の動作により)1行に表示され、
最後に関数が返した値が表示される。
関数の最後の式はwhile
ループであり、while
ループはつねにnil
を返すので、リストの最後の要素のあとに、nil
が表示される。
終了すべきときに止まらないループは無意味である。 ループをリストで制御する以外に、ループを止める一般的な方法は、 必要回数の繰り返しを完了したら偽を返すような第1引数を書くことである。 つまり、ループにカウンタ、ループの繰り返し回数を数える式 を持たせるのである。
(< count desired-number)
のように判定条件を記述すれば、
count
の値が繰り返し回数desired-number
より小さければ真を返し、
count
の値がdesired-number
に等しいか大きければ偽を返す。
カウンタを増加させる式は(setq count (1+ count))
のような
簡単なsetq
でよく、1+
は引数に1を加えるEmacs Lispの
組み込み関数である
(式(1+ count)
は、(+ count 1)
と同じ結果をもたらし、
人間にも読みやすい)。
カウンタを増加して制御するwhile
ループの雛型はつぎのようになる。
カウンタに初期値を設定 (while (< count desired-number) ; 判定条件 本体... (setq count (1+ count))) ; 1増やす
count
の初期値を設定する必要があることに注意してほしい。
普通は1に設定する。
浜辺で遊んでいるときに、つぎに示すように、
最初の行には小石を1個、つぎの行には2個、そのつぎの行には3個というように、
小石で三角形を作ろうと考えたとする。
(約2500年前に、ピタゴラスや他の人達は、 このような問題を考察して数論の始まりを築いた。)
7行の三角形を作るには何個の小石が必要か知りたいとしよう。
明らかに、必要なことは数1から7までを加算することである。
これには2つの方法がある。
最小数1から始めて、1、2、3、4のように加算する。
あるいは、最大数から始めて、7、6、5、4のように加算する。
いずれの方法も、while
ループを書く一般的な方法の例示になるので、
増やしながらの加算と減らしながらの加算の2つの例を書くことにする。
まずは、1、2、3、4と加算する例から始める。
数個の数を加算するだけならば、もっとも簡単な方法は、 それらをいっきに加算することである。 しかし、あらかじめ何個の数を加算するかわかっていなかったり、 非常に多くの数の加算にも対処したければ、 複雑な処理をいっきに行うのではなく、 単純な処理を繰り返すような加算にする必要がある。
たとえば、小石の個数をいっきに加算するかわりに、 最初は1行目の小石の個数1を2行目の個数2に加え、 これらの総和を3行目の個数3に加える。 つぎに、4行目の個数4を1行目から3行目までの総和に加えるということを繰り返す。
処理の重要な点は、繰り返しの各段階の動作は単純であることである。 この例では、各段階では、2つの数、つまり、 その行の小石の個数とそれまでの総和を加算するだけである。 最後の行をそれまでの総和に加算するまで、 2つの数の加算処理を繰り返し繰り返し実行するのである。 より複雑なループでは、各段階の動作は単純ではないかもしれないが、 すべてをいっきに行うよりは簡単である。
以上の分析により、関数定義の骨格がわかる。
まず、小石の総数を保持する変数total
が必要である。
これは、関数が返す値である。
つぎに、関数には引数が必要である。
この引数は三角形の行数である。
これをnumber-of-rows
としよう。
最後に、カウンタとして使う変数が必要である。
この変数をcounter
と命名してもよいが、
より適したrow-number
を使おう。
カウンタが数えるのは行数であり、
プログラムではできる限りわかりやすい名前を使うべきだからである。
Lispインタープリタが関数内の式の評価を始めるときには、
total
には何も加算していないので、
total
の値は0になっているべきである。
続いて、関数では、1行目の小石の個数を総和に加算し、
2行目の小石の個数を総和に加算し、3行目の小石の個数を総和に加算し、
ということを、加算すべき行がなくなるまで行う。
total
とrow-number
のどちらも、関数の内部だけで使うので、
let
でローカル変数として宣言し初期値を与える。
明らかに、total
の初期値は0である。
また、第1行から始めるので、row-number
の初期値は1である。
つまり、let
文はつぎのようになる。
(let ((total 0) (row-number 1)) 本体...)
内部変数を宣言しそれらに初期値を束縛したら、while
ループを始められる。
判定条件の式は、row-number
がnumber-of-rows
より小さいか等しい
限りは真を返す必要がある
(行の番号が三角形の行数より小さい場合に限り真を返す判定条件だと、
最後の行が総和に加算されない。
したがって、行の番号は三角形の行数より小さいか等しい必要がある)。
Lispには、第1引数が第2引数より小さいか等しいときに真を返し、
それ以外のときには偽を返す関数<=
がある。
したがって、while
が判定条件として評価する式はつぎのようになる。
(<= row-number number-of-rows)
小石の総数は、すでにわかっている総数に行の小石の個数を加算することを 繰り返して計算できる。 行の小石の個数はその行の番号に等しいので、 総数は、総数に行番号を加算すれば計算できる (より複雑な状況では、行の小石の個数は、より複雑な方法で行の番号に関係する。 そのような場合には、行の番号を適当な式で置き換える)。
(setq total (+ total row-number))
これにより、total
の新たな値は、行の小石の個数をそれまでの総数に
加えたものになる。
total
の値を設定したあと、
ループのつぎの繰り返しのための条件を確立する必要がある。
これには、カウンタとして働く変数row-number
の値を増やせばよい。
変数row-number
を増やしたあと、while
ループの判定条件により、
その値がnumber-of-rows
より小さいか等しいかどうか検査する。
もしそうならば、変数row-number
の新たな値を
ループのまえの段階でのtotal
に加える。
Emacs Lispの組み込み関数1+
は数に1を加えるので、
つぎの式で変数row-number
を増加できる。
(setq row-number (1+ row-number))
関数定義の各部分を作ったので、それらを1つにまとめよう。
まず、while
式の内容はつぎのとおりである。
(while (<= row-number number-of-rows) ; 判定条件 (setq total (+ total row-number)) (setq row-number (1+ row-number))) ; 増加
let
式の引数リストを加えれば、これで関数定義の本体はぼぼ完成する。
しかし、その必要性は少々微妙ではあるが、最後の要素が必要である。
つまり、while
式のあとに、変数total
だけを置いた行が必要である。
さもないと、関数全体としての戻り値は、最後の式の値、
つまり、let
の本体を評価した値、
つまり、while
が返す値であるが、これはつねにnil
である。
一見しただけでは、これは自明ではないかもしれない。
関数全体としての最後の式は、増加する式であると思うだろう。
しかし、その式はシンボルwhile
で始まるリストの最後の要素であり、
while
の本体の一部である。
さらに、while
ループ全体も、let
の本体の中にあるリストである。
関数の概略はつぎのとおりである。
(defun 関数名 (引数リスト) "説明文..." (let (変数リスト) (while (判定条件) whileの本体... ) ... ) ; 最後の式をここに置く
let
は、defun
全体としてのリスト以外のリストの内側にはないので、
let
を評価した結果がdefun
が返す値となる。
しかし、while
がlet
式の最後の要素だったとすると、
関数はつねにnil
を返す。
これは、われわれが望むことではない!
変数total
の値を返したいのである。
それには、let
で始まるリストの最後の要素として、
そのシンボルを書けばよい。
これにより、リストのまえのものが評価されたあとに、
正しい総数が設定されてから評価される。
このことは、let
で始まるリストを1行に書くとわかりやすいであろう。
変数リストとwhile
式は、let
で始まるリストの
第2要素と第3要素であり、total
は最後の要素であることがはっきりする。
(let (変数リスト) (while (判定条件) whileの本体... ) total)
以上をまとめると、triangle
の関数定義はつぎのとおりである。
(defun triangle (number-of-rows) ; 増加カウンタを利用した版 "Add up the number of pebbles in a triangle. The first row has one pebble, the second row two pebbles, the third row three pebbles, and so on. The argument is NUMBER-OF-ROWS." (let ((total 0) (row-number 1)) (while (<= row-number number-of-rows) (setq total (+ total row-number)) (setq row-number (1+ row-number))) total))
関数を評価してtriangle
をインストールすれば、試すことができる。
2つの例をあげておく。
(triangle 4) (triangle 7)
最初の4つの数を総和したものは10であり、最初の7つを総和したものは28である。
while
ループを書く一般的な別の方法は、
カウンタが0より大きいかを調べる検査を使うことである。
カウンタが0より大きい限り、ループを繰り返す。
しかし、カウンタが0になるか0より小さくなったら、ループを止める。
このように動作させるには、カウンタを0より大きく設定しておき、
繰り返すたびにカウンタを小さくするのである。
counter
の値が0より大きければ真t
を返し、
counter
の値が0に等しいか小さければ偽nil
を返す
(> counter 0)
のような式を判定条件に使う。
数を減らす式は、(setq counter (1- counter))
のような単純なsetq
でよく、1-
は引数から1を引くEmacs Lispの組み込み関数である。
減算によるwhile
ループの雛型はつぎのようになります。
(while (> counter 0) ; 判定条件 本体... (setq counter (1- counter))) ; 1減少
減少カウンタによるループの例として、カウンタを0まで減少させるように
関数triangle
を書き換える。
これは、この関数の前版の逆である。 つまり、3行の三角形を作るために必要な小石の個数は、 3行目の小石の個数3をそのまえの個数2に加え、2つの行の総数を そのまえの個数1に加えて計算する。
同様に、7行の三角形の小石の個数は、 7行目の小石の個数7をまえの行の個数6に加え、2つの行の総数を それらのまえの行の個数5に加え、と繰り返して計算する。 まえの例と同様に、各加算では2つの数、すでに加算した行の総数と、 加算すべき行の小石の個数を扱う。 2つの数を加える処理を、加えるべき小石がなくなるまで繰り返す。
始めの小石の個数はわかっている。 最後の行の小石の個数は、その行の番号に等しい。 7行の三角形の場合、最後の行の小石の個数は7である。 同様に、1つまえの行の小石の個数もわかっていて、行の番号より1つ小さい。
3つの変数が必要である。
三角形の行の数、行の小石の個数、求めたい小石の総数である。
これらの変数をそれぞれ、number-of-rows
、
number-of-pebbles-in-row
、total
としよう。
total
とnumber-of-pebbles-in-row
のいずれも関数の内側だけで
使うので、これらはlet
で宣言する。
total
の初期値は、当然、0である。
しかし、もっとも長い行から加算を始めるので、
number-of-pebbles-in-row
の初期値は、
三角形の行数に等しい必要がある。
つまり、let
式の始めの部分はつぎのようになる。
(let ((total 0) (number-of-pebbles-in-row number-of-rows)) 本体...)
小石の総数は、行の小石の個数をすでにわかっている総数に加算すればよく、 つぎの式を繰り返し評価すればよい。
(setq total (+ total number-of-pebbles-in-row))
number-of-pebbles-in-row
をtotal
に加算したあと、
ループのつぎの繰り返しでは、まえの行を総和に加算するので、
number-of-pebbles-in-row
を1減らす必要がある。
まえの行の小石の個数は、今の行の小石の個数より1小さいので、
Emacs Lispの組み込み関数1-
を使って、まえの行の小石の個数を計算できる。
これはつぎの式になる。
(setq number-of-pebbles-in-row (1- number-of-pebbles-in-row))
while
ループは行の小石がなくなったら繰り返しを止める。
したがって、while
ループの判定条件は単純である。
(while (> number-of-pebbles-in-row 0)
以上の式をまとめると、つぎのような関数定義を得る。
;;; 最初の減算版 (defun triangle (number-of-rows) "Add up the number of pebbles in a triangle." (let ((total 0) (number-of-pebbles-in-row number-of-rows)) (while (> number-of-pebbles-in-row 0) (setq total (+ total number-of-pebbles-in-row)) (setq number-of-pebbles-in-row (1- number-of-pebbles-in-row))) total))
この関数は正しく動作する。
しかし、1つのローカル変数number-of-pebbles-in-row
は、
不要であることがわかる。
関数triangle
を評価するとき、
シンボルnumber-of-rows
には数が束縛され、それが初期値になる。
その数を変更しても関数の外側の変数の値に影響することはなく、
ローカル変数であるかのように関数の本体でその数を変更できる。
これはLispのとても有用な特徴である。
つまり、関数内部では、number-of-pebbles-in-row
のかわりに
変数number-of-rows
を使えるのである。
簡素に書き直した、この関数の第2版を示す。
(defun triangle (number) ; 第2版 "Return sum of numbers 1 through NUMBER inclusive." (let ((total 0)) (while (> number 0) (setq total (+ total number)) (setq number (1- number))) total))
まとめると、正しく書かれたwhile
ループは3つの部分から成る。
再帰的関数では、自分自身の評価を指示するコードを含む。 関数が自分自身を評価するとき、自分自身の評価を指示するコードを 再度みつけるので、関数は自分自身を再度評価する...を繰り返す。 再帰的関数は、停止条件が与えられない限り、 自身を評価することを永久に続ける。
典型的な再帰的関数には、3つの部分から成る条件式が含まれる。
再帰的関数は、他の種類の関数よりもとても簡単である。 もちろん、これを使い始めたばかりの人には、 理解できないほど不可思議に単純に見える。 自転車に乗るのと同じように、再帰的関数定義を読むには、 最初は難しくてもしだいに簡単に思えるようになるコツが必要である。
再帰的関数の雛型はつぎのようになる。
(defun 再帰的関数名 (引数リスト) "説明文..." 本体... (if 再帰条件 (再帰的関数名 次段式)))
再帰的関数を評価するたびに、引数には次段式の値が束縛され、 その値を再帰条件で使う。 関数をそれ以上繰り返さない場合には、再帰条件が偽になるように次段式を作る。
再帰条件が偽になると繰り返しを停止するので、 再帰条件を停止条件(stop condition)と呼ぶこともある。
while
loop with recursion.
動物のリストの各要素を表示するwhile
ループの例を
再帰的に書くことができる。
変数animals
にリストを設定する式も含めて、そのコードを示す。
この例は、バッファ*scratch*
にコピーしてから、
そこで個々の式を評価する必要がある。
結果がバッファに表示されるように、C-u C-x C-eを使って
式(print-elements-recursively animals)
を評価すること。
さもないと、Lispインタープリタは結果をエコー領域の1行に押し込めて表示する。
また、関数print-elements-recursively
の注釈のまえの
最後の閉じ括弧の直後にカーソルを置くこと。
さもないと、Lispインタープリタは注釈を評価しようとする。
(setq animals '(giraffe gazelle lion tiger)) (defun print-elements-recursively (list) "Print each element of LIST on a line of its own. Uses recursion." (print (car list)) ; 本体 (if list ; 再帰条件 (print-elements-recursively ; 再帰呼び出し (cdr list)))) ; 次段式 (print-elements-recursively animals)
関数print-elements-recursively
は、まず、リストの先頭要素、
つまり、リストのCARを表示する。
続いて、リストが空でなければ、関数は自分自身を呼び出すが、
その引数には、リスト全体ではなく、リストの2番目以降の要素、
つまり、リストのCDRを渡す。
これを評価すると、受け取った引数(もとのリストの2番目以降の要素)の先頭要素を
表示する。
続いて、if
式を評価し、真ならば、リストのCDR、
つまり、(2回目なので)もとのリストのCDRのCDRを
引数として自身を呼び出す。
関数が自身を呼び出すごとに、もとのリストを短くしたものを引数に渡す。
最終的に、空リストで自身を呼び出す。
関数print
は空リストをnil
と表示する。
つぎに、条件式ではlist
の値を調べる。
list
の値はnil
なので、if
式の判定条件は偽になり、
真の場合の動作は評価されない。
そして、関数全体としてはnil
を返す。
そのため、この関数を評価するとnil
が2つ表示されるのである。
バッファ*scratch*
で(print-elements-recursively animals)
を
評価するとつぎのようになる。
giraffe gazelle lion tiger nil nil
(最初のnil
は空リストの値を表示したもので、
2番目のnil
は関数全体としての戻り値である。)
前節で説明した関数triangle
を再帰で書くこともできる。
つぎのようになる。
(defun triangle-recursively (number) "Return the sum of the numbers 1 through NUMBER inclusive. Uses recursion." (if (= number 1) ; 再帰条件 1 ; 真の場合の動作 (+ number ; 偽の場合の動作 (triangle-recursively ; 再帰呼び出し (1- number))))) ; 次段式 (triangle-recursively 7)
これを評価して関数をインストールすれば、
(triangle-recursively 7)
を評価して試すことができる
(注釈のまえの関数定義の最後の括弧の直後にカーソルを置くこと)。
この関数の動作を理解するために、 引数として、1、2、3、4をこの関数に渡すとどうなるかを考えよう。
まず、引数が1の場合はどうなるだろうか?
関数には、説明文字列に続けてif
式がある。
これはnumber
の値が1に等しいかどうかを調べる。
等しければ、Emacsはif
式の真の場合の動作を評価し、
関数の値として数1を返す
(1行の三角形には1個の小石がある)。
では、引数の値が2の場合を考えよう。
この場合は、Emacsはif
式の偽の場合の動作を評価する。
偽の場合の動作は、加算、triangle-recursively
の再帰呼び出し、
減算動作から成り、つぎのとおりである。
(+ number (triangle-recursively (1- number)))
Emacsがこの式を評価するとき、もっとも内側の式が最初に評価され、 つぎに、それ以外の部分が評価される。 詳しくはつぎのような手順になる。
(1- number)
であり、
Emacsはnumber
の値を2から1へ減らす。
triangle-recursively
を評価する。
triangle-recursively
を呼び出す。
この場合、Emacsは引数1でtriangle-recursively
を評価する。
つまり、triangle-recursively
を評価すると1が返される。
number
の値を評価する。
number
は、+
で始まるリストの2番目の要素であり、
その値は2である。
+
式の評価。
+
式は2つの引数、
number
の評価結果(手順3)と、triangle-recursively
の評価結果
(手順2)を受け取る。
加算の結果は、2足す1で、数3が返される。 これは正しい値である。 2行の三角形には3個の小石がある。
引数3でtriangle-recursively
が呼ばれた場合を考えよう。
if
式が評価される。
これは再帰条件であり偽を返すから、if
式の偽の場合の動作が評価される
(この例では、再帰条件が偽の場合に自身を呼び出すのであり、
真の場合には呼ばない)。
triangle-recursively
を評価する。
triangle-recursively
に数2が渡される。
Emacsが、引数2でtriangle-recursively
を評価するとどうなるかは
すでに知っている。
上で述べたような動作順序のあとで、値3が返される。
ここでもそのようになる。
関数全体として返す値は、6になる。
これで、引数3でtriangle-recursively
を呼ぶとどうなるかがわかった。
引数4で呼んだ場合にどうなるかも明らかであろう。
再帰呼び出しにおいて、(triangle-recursively (1- 4))の評価結果は、つぎの式の評価結果であり、
(triangle-recursively 3)これは6であり、この値が3行目で4に加えられる。
関数全体として返す値は10である。
triangle-recursively
を評価するたびに、
引数が小さくなりすぎて評価できなくなるまで、より小さな引数で自身を評価する。
cond
を用いた再帰の例triangle-recursively
の上の版は、
スペシャルフォームif
を用いて書いた。
別のcond
と呼ばれるスペシャルフォームを使っても書ける。
スペシャルフォームcond
の名前は、
単語conditional
の略である。
スペシャルフォームcond
は、Emacs Lispのソースでは、
if
ほど多用されないが、ここで説明するに十分なほど使われる。
cond
式の雛型はつぎのとおりである。
(cond 本体...)
本体は、一連のリストである。
より詳しく書くと、雛型はつぎのとおりである。
(cond ((最初の判定条件 最初の帰結動作) (2番目の判定条件 2番目の帰結動作) (3番目の判定条件 3番目の帰結動作) ...)
Lispインタープリタがcond
式を評価するとき、
cond
の本体の一連の式の最初の式の最初の要素
(CAR、つまり、判定条件)を評価する。
判定条件がnil
を返すと、式の残り、つまり、帰結動作は飛び越され、
つぎの式の判定条件を評価する。
判定条件がnil
以外の値を返す式がみつかると、
その式の帰結動作を評価する。
帰結動作は、複数個の式でよい。
帰結動作が複数個の式の場合、それらの式は順番に評価され、
最後のものの値が返される。
式に動作がない場合には、判定条件の値が返される。
真となる判定条件がない場合には、cond
式はnil
を返す。
cond
を用いて書くと、関数triangle
はつぎのようになる。
(defun triangle-using-cond (number) (cond ((<= number 0) 0) ((= number 1) 1) ((> number 1) (+ number (triangle-using-cond (1- number))))))
この例では、cond
は、数が0より小さいか等しい場合には0を返し、
数が1の場合には1を返し、
数が1より大きい場合には、(+ number (triangle-using-cond (1- number)))
を
評価する。
triangle
に似た関数で、各行には行の番号の2乗の値があるような関数を
書いてみよ。
while
を使うこと。
triangle
に似た関数で、加算のかわりに乗算を使う関数を書いてみよ。
cond
を使うように直してみよ。
@dfn
に対するインデックスをその段落の始めに作るような
Texinfoモード用の関数を書いてみよ
(Texinfoファイルでは、@dfn
は定義に印を付ける。
詳しくは、
正規表現の探索は、GNU Emacsでは非常に多用されている。
2つの関数forward-sentence
とforward-paragraph
は、
これらの探索のよい例である。
正規表現の探索に
ついては、Regexp SearchやRegular Expressionsに記述されている。
本章を執筆するにあたり、読者はこれらに関して少なくともある程度の知識を
持っていると仮定した。
主要な点は、正規表現により、文字列の字面どおりの探索に加えて、
パターンの探索もできることである。
たとえば、forward-sentence
のコードは、文末を表す可能性のある
文字のパターンを探索し、そこへポイントを移動する。
関数forward-sentence
のコードを説明するまえに、
文末を表すパターンとはどんなものであるかを考えることも価値がある。
このパターンについては次節で説明する。
続いて、正規表現の探索関数re-search-forward
を説明する。
さらに、関数forward-sentence
を説明する。
そして、本章の最後の節では、関数forward-paragraph
を説明する。
forward-paragraph
は複雑な関数であり、新たな機能もいくつか紹介する。
sentence-end
.
search-forward
.
TAGS
table.
sentence-end
のための正規表現シンボルsentence-end
は、文末を表すパターンに束縛される。
この正規表現はどんなものであるべきか?
明らかに、文は、ピリオドや疑問符や感嘆符で終わる。 もちろん、これら3つの文字のうちの1つで終わる節のみを文末と考えるべきである。 つまり、パターンにはつぎの文字集合が含まれるべきである。
[.?!]
しかし、ピリオドや疑問符や感嘆符は文の途中に使われることもあるので、
forward-sentence
が単純にこれら3つの文字に移動してほしくはない。
たとえば、ピリオドは省略形のあとにも使われる。
つまり、別の情報が必要なのである。
慣習としては、文のうしろには2つの空白を置くが、 文の途中のピリオドや疑問符や感嘆符のうしろには空白を1つだけ置く。 つまり、ピリオドや疑問符や感嘆符のあとに空白が2つあれば、 文末を表すと考えられる。 しかし、ファイルでは、2つの空白のかわりに、タブだったり行末だったりもする。 つまり、正規表現は、これらの3つの場合を含む必要がある。 これらは、つぎのように表せる。
\\($\\| \\| \\) ^ ^^ TAB SPC
ここで、$
は行末を表し、また、式の中にタブがあるのか空白が2つあるのか
わかるようにしてある。
どちらも式の中には実際の文字を入れる。
括弧と縦棒のまえには2つのバックスラッシュ\\
が必要である。
Emacsでは、最初のバックスラッシュはそれに続くバックスラッシュをクオートし、
2番目のバックスラッシュは、それに続く括弧や縦棒が特別な文字であることを表す。
また、つぎのように、文には複数個の復帰が続いてもよい。
[ ]*
タブや空白のように、正規表現に復帰を入れるにはそのまま入れる。 アスタリスクは、<RET>が0回以上繰り返されることを表す。
文末は、ピリオドや疑問符や感嘆符のあとに適切な空白が続くだけはない。 空白のまえが、閉じ引用符や閉じ括弧の類でもよい。 もちろん、空白のまえにこれらが複数個あってもよい。 これらはつぎのような正規表現を必要とする。
[]\"')}]*
この式で、最初の]
は正規表現の最初の文字である。
2番目の文字は"
であり、そのまえには\
があるが
Emacsに"
が特別な文字ではないことを指示する。
残りの3つの文字は'
と)
と}
である。
これらすべてで、文末に一致する正規表現のパターンが何であるかを表す。
そして、もちろん、sentence-end
を評価すると
つぎのような値になっていることがわかる。
sentence-end => "[.?!][]\"')}]*\\($\\| \\| \\)[ ]*"
re-search-forward
関数re-search-forward
は、関数search-forward
にとてもよく似ている
(See search-forward)。
re-search-forward
は正規表現を探索する。
探索に成功すると、みつかった文字列の最後の文字の直後にポイントを置く。
逆向きの探索の場合には、みつかった文字列の最初の文字の直前にポイントを置く。
真値としてt
を返すようにre-search-forward
に指示できる
(したがって、ポイントの移動は「副作用」である)。
search-forward
のように、関数re-search-forward
は4つの引数を取る。
nil
を指定すると、探索に失敗した場合、
関数はエラーを通知(し、メッセージを表示)する。
それ以外の値を指定すると、探索に失敗した場合はnil
を返し、
探索に成功するとt
を返す。
re-search-forward
に逆向き探索を指示する。
re-search-forward
の雛型はつぎのとおりである。
(re-search-forward "正規表現" 探索範囲 探索失敗時の動作 繰り返し回数)
第2引数、第3引数、第4引数は省略できる。 しかし、最後の2つの引数のいずれか、あるいは、両者に値を渡したい場合には、 そのまえにある引数すべてに値を渡す必要がある。 さもないと、Lispインタープリタはどの引数に値を渡すのかを誤解することになる。
関数forward-sentence
では、
正規表現は変数sentence-end
の値であり、つぎのとおりである。
"[.?!][]\"')}]*\\($\\| \\| \\)[ ]*"
探索の範囲は、(文は段落を越えることはないので)段落の末尾までである。
探索に失敗すると、関数はnil
を返す。
繰り返し回数は、関数forward-sentence
の引数で与える。
forward-sentence
カーソルを文単位で先へ進めるコマンドは、 Emacs Lispにおいて正規表現の探索の使い方を示す直接的な例である。 もちろん、関数は単なる例よりは長くて複雑である。 これは、関数が前向きと同時に逆向き探索にも対応しており、 文単位で複数回進めることもできるからである。 この関数は、通常、M-eのキーコマンドにバインドされている。
forward-sentence
のコードをつぎに示す。
(defun forward-sentence (&optional arg) "Move forward to next sentence-end. With argument, repeat. With negative argument, move backward repeatedly to sentence-beginning. Sentence ends are identified by the value of sentence-end treated as a regular expression. Also, every paragraph boundary terminates sentences as well." (interactive "p") (or arg (setq arg 1)) (while (< arg 0) (let ((par-beg (save-excursion (start-of-paragraph-text) (point)))) (if (re-search-backward (concat sentence-end "[^ \t\n]") par-beg t) (goto-char (1- (match-end 0))) (goto-char par-beg))) (setq arg (1+ arg))) (while (> arg 0) (let ((par-end (save-excursion (end-of-paragraph-text) (point)))) (if (re-search-forward sentence-end par-end t) (skip-chars-backward " \t\n") (goto-char par-end))) (setq arg (1- arg))))
一見すると関数は長いが、骨格を見てからその筋肉を見るのが最良であろう。 骨格を見るには、もっとも左のコラムから始まる式を見ればよい。
(defun forward-sentence (&optional arg) "説明文..." (interactive "p") (or arg (setq arg 1)) (while (< arg 0) whileループの本体 (while (> arg 0) whileループの本体
だいぶ簡単に見える。
関数定義は、説明文、interactive
式、or
式、
while
ループから成る。
これらの各部分を順番に見てみよう。
説明文は、十分でわかりやすい。
関数には、interactive "p"
の宣言がある。
これは、もしあれば処理した前置引数を引数として関数に渡すことを意味する
(これは数である)。
関数に(省略できる)引数が渡されないと、引数arg
には1が束縛される。
forward-sentence
が非対話的に引数なしで呼ばれた場合には、
arg
にはnil
が束縛される。
or
式で前置引数を処理する。
これは、arg
に値が束縛されていればそのままにするが、
arg
にnil
が束縛されているときにはarg
の値を1にする。
while
loops.
while
ループor
式に続けて2つのwhile
ループがある。
最初のwhile
には、forward-sentence
の前置引数が負の数のときに
真となる判定条件がある。
これは、逆向き探索用である。
このループの本体は2番目のwhile
節の本体に似ているが、同一ではない。
このwhile
ループを飛ばして、2番目のwhile
に注目しよう。
2番目のwhile
ループは、ポイントを先へ進めるためのものである。
その骨格はつぎのように読める。
(while (> arg 0) ; 判定条件
(let 変数リスト
(if (判定条件)
真の場合の動作
偽の場合の動作
(setq arg (1- arg)))) ; while
ループのカウンタを減らす
while
ループは減少方式である
(See Decrementing Loop)。
これは、カウンタ(変数arg
)が0より大きい限り真を返す判定条件と、
ループを1回廻るごとにカウンタの値を1減らす減少式を持つ。
コマンドのもっとも一般的な用法であるが、
forward-sentence
に前置引数を与えないとarg
の値は1なので、
このwhile
ループは1回だけ廻る。
while
ループの本体は、let
式から成り、
ローカル変数を作って束縛し、本体はif
式である。
while
ループの本体はつぎのとおりである。
(let ((par-end (save-excursion (end-of-paragraph-text) (point)))) (if (re-search-forward sentence-end par-end t) (skip-chars-backward " \t\n") (goto-char par-end)))
let
式は、ローカル変数par-end
を作って束縛する。
あとでわかるように、このローカル変数は、正規表現の探索の範囲を
制限するためのものである。
段落の中で正しい文末を探せなかった場合には、段落の末尾で止まる。
まず、どのようにしてpar-end
に段落の末尾が束縛されるかを説明する。
let
は、par-end
の値に、
Lispインタープリタがつぎの式を評価した値を設定する。
(save-excursion (end-of-paragraph-text) (point))
この式では、(end-of-paragraph-text)
でポイントを段落の末尾に移動し、
(point)
でポイントの値を返す。
そして、save-excursion
がポイントをもとの位置に戻す。
したがって、let
は、save-excursion
が返した値をpar-end
に
束縛するが、これは段落の末尾の位置である
(関数(end-of-paragraph-text)
は、これから説明する
forward-paragraph
を使う)。
Emacsは、続いて、let
の本体を評価する。
それはつぎのようなif
式である。
(if (re-search-forward sentence-end par-end t) ; 判定条件 (skip-chars-backward " \t\n") ; 真の場合の動作 (goto-char par-end))) ; 偽の場合の動作
if
は、第1引数が真かどうかを調べ、
そうならば、真の場合の動作を評価し、さもなければ、
Emacs Lispインタープリタは偽の場合の動作を評価する。
if
式の判定条件は、正規表現の探索である。
関数forward-sentence
のこのような実際の動作は奇妙に思えるかもしれないが、
これはLispでこの種の操作を行う一般的な方法である。
関数re-search-forward
は文末を探す、
つまり、正規表現sentence-end
で定義されたパターンを探す。
パターンがみつかれば、つまり、文末がみつかれば、
関数re-search-forward
はつぎの2つのことを行う。
re-search-forward
は、副作用を及ぼす。
つまり、みつけた文字列の直後にポイントを移動する。
re-search-forward
は真値を返す。
この値をif
が受け取り、探索が成功したことを知る。
ポイントを移動するという副作用は、関数if
に探索成功の値が
返されるまえに完了する。
関数if
が、re-search-forward
の呼び出しに成功し真値を受け取ると、
if
は真の場合の動作、式(skip-chars-backward " \t\n")
を評価する。
この式は、目に見える文字がみつかるまで逆向きに空白やタブや復帰を飛ばして
目に見える文字の直後にポイントを移動する。
ポイントは文末のパターンの直後にあったので、
この操作により、ポイントは文末の目に見える文字、
普通はピリオドの直後にポイントを置く。
一方、関数re-search-forward
が文末のパターンの検索に失敗すると、
関数は偽を返す。
するとif
は第3引数、つまり、(goto-char par-end)
を評価し、
段落の末尾にポイントを移動する。
正規表現の探索はとても便利であり、if
式の判定条件でも使っている
re-search-forward
はパターンの例題として簡便である。
読者もこのパターンをしばしば利用するであろう。
forward-paragraph
:関数の宝庫関数forward-paragraph
は、段落の末尾にポイントを進める。
普通はM-}にバインドされており、
let*
やmatch-beginning
やlooking-at
の
それ自体で重要な関数を利用している。
詰め込み接頭辞(fill prefix)で始まる行から成る段落を処理するため、
forward-paragraph
の関数定義は、forward-sentence
の関数定義に
比べてとても長い。
詰め込み接頭辞は、各行の先頭で繰り返すことができる文字の文字列から成る。
たとえば、Lispコードでは、段落ほどもある注釈の各行は;;;
で
始める約束になっている。
テキストモードでは4つの空白を詰め込み接頭辞とするのが一般的であり、
字下げした段落になる
(詰め込み接頭辞について詳しくは、
See Fill Prefix)。
詰め込み接頭辞があると、関数forward-paragraph
は、
もっとも左のコラムから始まる行から成る段落の末尾を探すだけでなく、
詰め込み接頭辞で始まる行を含む段落の末尾も探す必要がある。
さらに、段落を区切る空行の場合には、 詰め込み接頭辞を無視するほうが実用的である。 これも複雑さが増す理由である。
関数forward-paragraph
の全体を示すかわりに、その一部だけを示す。
前準備もなしに読むと、気力をくじかれる!
関数の概略はつぎのとおりである。
(defun forward-paragraph (&optional arg) "説明文..." (interactive "p") (or arg (setq arg 1)) (let* 変数リスト (while (< arg 0) ; 戻すコード ... (setq arg (1+ arg))) (while (> arg 0) ; 進めるコード ... (setq arg (1- arg)))))
関数の始めの部分は決まりきっていて、 省略できる1個の引数から成る関数の引数リストである。 これに説明文が続く。
宣言interactive
の小文字のp
は、もしあれば処理した前置引数を
関数に渡すことを意味する。
これは数であり、何個の段落だけ先へ進むかを表す繰り返し回数である。
つぎのor
式は、関数に引数が渡されなかった場合の共通処理で、
対話的にではなく他のコードから関数が呼び出された場合にこうなる
(See forward-sentence)。
この関数の馴染みのある部分はこれで終わりである。
let*
expression.
while
loop.
forward-paragraph
code.
let*
式関数forward-paragraph
のつぎの行はlet*
式で始まる。
これはこれまでに見てきた式とは種類が異なる。
シンボルはlet*
であり、let
ではない。
スペシャルフォームlet*
はlet
に似ているが、
Emacsは1つずつ順に各変数を設定し、変数リストのうしろの変数では、
Emacsがすでに設定した変数リストのまえの部分の変数の値を利用してもよいところが
異なる。
この関数のlet*
式では、Emacsは2つの変数、
fill-prefix-regexp
とparagraph-separate
を束縛する。
paragraph-separate
に束縛される値は、
fill-prefix-regexp
に束縛された値に依存する。
それぞれを順番に見てみよう。
シンボルfill-prefix-regexp
には、つぎのリストを評価した値が設定される。
(and fill-prefix (not (equal fill-prefix "")) (not paragraph-ignore-fill-prefix) (regexp-quote fill-prefix))
この式の先頭要素は関数and
である。
関数and
は、引数の1つが値nil
を返すまで各引数を評価する。
nil
を返した場合には、and
式もnil
を返す。
しかし、値nil
を返す引数がなければ、最後の引数を評価した値を返す
(そのような値はnil
以外なので、Lispでは真と解釈される)。
いいかえれば、and
式は、すべての引数が真であるときに限り真値を返す
ここでは、つぎの4つの式を評価して真値(つまり、非nil
)が得られれば、
変数fill-prefix-regexp
には非nil
の値が束縛される。
さもなければ、fill-prefix-regexp
はnil
に束縛される。
fill-prefix
nil
を返す。
(not (equal fill-prefix "")
(not paragraph-ignore-fill-prefix)
paragraph-ignore-fill-prefix
に
t
などの真値を設定してオンになっているとnil
を返す。
(regexp-quote fill-prefix)
and
の最後の引数である。
and
のすべての引数が真ならば、
この式を評価した結果の値をand
式が返し、
変数fill-prefix-regexp
に束縛される。
このand
式が正しく評価されると、fill-prefix-regexp
には、
関数regexp-quote
で修正したfill-prefix
の値が束縛される。
regexp-quote
は、文字列を読み取り、その文字列のみに一致し
それ以外には一致しない正規表現を返す。
つまり、fill-prefix-regexp
には、
詰め込み接頭辞があれば、詰め込み接頭辞だけに一致するものが設定される。
さもなければ、この変数にはnil
が設定される。
let*
式の2番目のローカル変数はparagraph-separate
である。
これにはつぎの式を評価した値が束縛される。
(if fill-prefix-regexp (concat paragraph-separate "\\|^" fill-prefix-regexp "[ \t]*$") paragraph-separate)))
この式は、let
ではなくlet*
を使った理由を示している。
if
の判定条件は、変数fill-prefix-regexp
がnil
であるか
それ以外の値であるかに依存している。
fill-prefix-regexp
に値がなければ、Emacsはif
式の偽の場合の動作を
評価して、ローカル変数にparagraph-separate
を束縛する
(paragraph-separate
は、段落の区切りに一致する正規表現である)。
一方、fill-prefix-regexp
に値があれば、
Emacsはif
式の真の場合の動作を評価して、
paragraph-separate
には、
パターンとしてfill-prefix-regexp
を含む正規表現を束縛する。
特に、paragraph-separate
には、段落を区切るもとの正規表現に、
fill-prefix-regexp
に空行が続くという代替パターンを連結したものを
設定する。
^
はfill-prefix-regexp
が行の先頭から始まることを指定し、
"[ \t]*$"
は行末に空白が続いてもよいことを指定する。
\\|
は、この部分がparagraph-separate
に対する
代替の正規表現であることを指定する。
ではlet*
の本体に移ろう。
let*
の本体の始めの部分は、関数に負の引数を与えた場合の処理であり、
逆向きに戻す。
本節では、割愛する。
while
ループlet*
の本体の2番目の部分は、先へ進める処理を行う。
これは、arg
の値が0より大きい限り繰り返すwhile
ループである。
関数のもっとも一般的な使い方では引数の値は1であり、
while
ループの本体がちょうど1回だけ評価され、
カーソルを1段落分進める。
この部分では3つの状況を処理する。 ポイントが段落のあいだにある場合、 ポイントが段落の中にあり詰め込み接頭辞がある場合、 ポイントが段落の中にあり詰め込み接頭辞がない場合である。
while
ループはつぎのとおりである。
(while (> arg 0) (beginning-of-line) ;; 段落のあいだ (while (prog1 (and (not (eobp)) (looking-at paragraph-separate)) (forward-line 1))) ;; 段落の中で、詰め込み接頭辞あり (if fill-prefix-regexp ;; 詰め込み接頭辞がある; 段落の始まりのかわりに使う (while (and (not (eobp)) (not (looking-at paragraph-separate)) (looking-at fill-prefix-regexp)) (forward-line 1)) ;; 段落の中で、詰め込み接頭辞なし (if (re-search-forward paragraph-start nil t) (goto-char (match-beginning 0)) (goto-char (point-max)))) (setq arg (1- arg)))
減少式として式(setq (1- arg))
を使っているので、
減少カウンタのwhile
ループであることはすぐにわかる。
ループの本体は3つの式から成る。
;; 段落のあいだ (beginning-of-line) (while whileの本体) ;; 段落の中で、詰め込み接頭辞あり (if 判定条件 真の場合の動作 ;; 段落の中で、詰め込み接頭辞なし 偽の場合の動作
Emacs Lispインタープリタがwhile
ループの本体を評価するとき、
最初に行うことは、式(beginning-of-line)
を評価して
ポイントを行の先頭に移動することである。
続いて、内側のwhile
ループがくる。
このwhile
ループは、段落のあいだに空行があれば、
そこからカーソルを移動するためのものである。
最後のif
式で、ポイントを段落の末尾に実際に移動する。
まず、内側のwhile
ループを説明しよう。
このループは、ポイントが段落のあいだにある場合を扱う。
3つの新たな関数、prog1
、eobp
、looking-at
を使っている。
prog1
は関数progn
に似ているが、
引数を順番に評価し終えたあとに、第1引数の値を式全体の値として返す点が異なる
(progn
は、式の値としては最後の引数の値を返す)。
prog1
の2番目以降の引数は、副作用のためだけに評価される。
eobp
はEnd Of Buffer(バッファの終わり)P
の略であり、
ポイントがバッファの最後にあるときに真を返す関数である。
looking-at
は、ポイントに続くテキストがlooking-at
の引数に
渡した正規表現に一致する場合に真を返す関数である。
説明しているwhile
ループはつぎのとおりである。
(while (prog1 (and (not (eobp)) (looking-at paragraph-separate)) (forward-line 1)))
このwhile
ループには本体がない!
ループの判定条件はつぎの式である。
(prog1 (and (not (eobp)) (looking-at paragraph-separate)) (forward-line 1)))
prog1
の第1引数はand
式である。
この中では、ポイントがバッファの最後にあるかどうか、
段落を区切る正規表現に一致するものがポイントに続いているかどうか、
を検査する。
カーソルがバッファの最後になくて、
カーソルに続く文字の列が段落を区切るものであれば、and
式は真になる。
and
式を評価したあと、Lispインタープリタはprog1
の第2引数、
forward-line
を評価する。
これは、ポイントを1行分先へ進める。
しかし、prog1
が返す値は第1引数の値であるため、
ポイントがバッファの最後になくて、かつ、
段落のあいだにあるかぎり、while
ループは繰り返される。
最終的にポイントが段落に達するとand
式は偽になる。
しかし、いずれにしてもコマンドforward-line
は実行されることに
注意してほしい。
つまり、段落と段落のあいだでポイントを移動したときには、
段落の第2行目の先頭にポイントが移動するのである。
外側のwhile
ループのつぎの式はif
式である。
変数fill-prefix-regexp
がnil
以外の値を持っている場合には、
Lispインタープリタはif
の真の場合の動作を評価し、
fill-prefix-regexp
の値がnil
の場合、
つまり、詰め込み接頭辞がない場合には偽の場合の動作を評価する。
詰め込み接頭辞がない場合のコードを見るほうが簡単である。
このコードは、さらに内側にif
式を含み、つぎのようになっている。
(if (re-search-forward paragraph-start nil t) (goto-char (match-beginning 0)) (goto-char (point-max)))
この式は、ほとんどの人がコマンドforward-paragraph
の主要な目的で
あると考えることを行う。
つぎの段落の先頭をみつけるための正規表現の探索を行い、
みつかればポイントをそこへ移動する。
段落の始まりがみつからなければ、バッファの参照可能なリージョンの最後に
ポイントを移動する。
この部分で馴染みがないのはmatch-beginning
の使い方であろう。
これもわれわれにとっては新しいものである。
関数match-beginning
は、直前の正規表現の探索で一致した
テキストの先頭位置を与える数を返す。
関数match-beginning
を使うのは、探索の性質のためである。
普通の探索であれ正規表現の探索であれ、
探索に成功するとポイントは探し出したテキストの終わりに移動する。
この場合では、探索に成功すると、
ポイントはparagraph-start
に一致したテキストの終わりに移動するが、
これは、今の段落の末尾ではなく、つぎの段落の始まりである。
しかし、つぎの段落の始まりにではなく、今の段落の末尾にポイントを 置きたいのである。 段落のあいだには何行かの空行がありえるので、2つの位置は異なるであろう。
引数に0を指定すると、match-beginning
は、直前の正規表現の探索で
一致したテキストの始まりの位置を返す。
この例では、直前の正規表現の探索は、paragraph-start
を探したものであり、
match-beginning
は、パターンの終わりではなく始まりの位置を返す。
始まりの位置は、段落の末尾である。
(引数に正の数を指定すると、関数match-beginning
は、
直前の正規表現の中の括弧表現にポイントを置く。
これは便利な機能である。)
説明したばかりの内側のif
式は、詰め込み接頭辞の有無を調べる
if
式の偽の場合の動作である。
詰め込み接頭辞がある場合には、このif
式の真の場合の動作が評価される。
それはつぎのとおりである。
(while (and (not (eobp)) (not (looking-at paragraph-separate)) (looking-at fill-prefix-regexp)) (forward-line 1))
この式は、つぎの3つの条件が真である限り、ポイントを1行進める。
このまえにある関数forward-paragraph
で行の先頭にポイントが移動している
ことを思い出さないと、最後の条件に惑わされるかもしれない。
つまり、テキストに詰め込み接頭辞がある場合、
関数looking-at
はそれをみつけるのである。
まとめると、関数forward-paragraph
がポイントを進めるときには、
つぎのことを行う。
復習のために、ここで説明したコードを わかりやすいように整形して以下に記す。
(interactive "p") (or arg (setq arg 1)) (let* ( (fill-prefix-regexp (and fill-prefix (not (equal fill-prefix "")) (not paragraph-ignore-fill-prefix) (regexp-quote fill-prefix))) (paragraph-separate (if fill-prefix-regexp (concat paragraph-separate "\\|^" fill-prefix-regexp "[ \t]*$") paragraph-separate))) ポインタをまえへ戻すコード(省略) ... (while (> arg 0) ; 進めるコード (beginning-of-line) (while (prog1 (and (not (eobp)) (looking-at paragraph-separate)) (forward-line 1))) (if fill-prefix-regexp (while (and (not (eobp)) ; 真の場合の動作 (not (looking-at paragraph-separate)) (looking-at fill-prefix-regexp)) (forward-line 1)) ; 内側のifの偽の場合の動作 (if (re-search-forward paragraph-start nil t) (goto-char (match-beginning 0)) (goto-char (point-max)))) (setq arg (1- arg))))) ; 減少式
関数のforward-paragraph
の完全な定義には、
以上の進めるコードに加えて戻るコードも含まれる。
GNU Emacsで読んでいて、関数全体を見たい場合には、
M-.(find-tag
)とタイプし、問い合わせに対して関数名を与える。
関数find-tag
がタグテーブルの名前を問い合わせてきたら、
読者のサイトのディレクトリemacs/src
のタグファイルの名前を与える。
ディレクトリemacs/src
は、
/usr/local/lib/emacs/19.23/src/TAGS
のようなパス名であろう
(ディレクトリemacs/src
の正確なパスは、
Emacsをどのようにインストールしたかに依存する。
わからない場合には、C-h iとタイプしてInfoに入り、
C-x C-fとタイプしてディレクトリemacs/info
のパスを調べる。
タグファイルはemacs/src
のパスに対応する場合が多いが、
infoファイルをまったく別の場所に置く場合もある)。
ディレクトリにタグファイルがなくても、
読者専用のTAGS
ファイルを作成できる。
ソースを調べ廻る際の補助として、個人用のタグファイルを作成できる。
筆者のディレクトリ~/emacs
には、137個の.el
ファイルがあり、
そのうちの17個をロードしている。
読者のディレクトリ~/emacs
にも多数のファイルがある場合、
タグファイルを作っておけば望みの関数にジャンプでき、
grep
などのツールで関数名を探すより簡単である。
タグファイルを作成するには、Emacsのディストリビューションに含まれる
プログラムetags
を使う。
普通、Emacsを作るときにetags
もコンパイルされてインストールされる
(etags
はEmacs Lispの関数でもEmacsの一部でもない。
Cのプログラムである)。
タグファイルを作るには、タグファイルを作りたいディレクトリにまず移動する。
Emacsの中では、コマンドM-x cdで行うか、
そのディレクトリのファイルを訪問するか、
C-x d(dired
)でディレクトリを表示する。
続いて、
M-! etags *.el
とタイプすれば、タグファイルTAGS
が作られる。
プログラムetags
では、普通のシェルの「ワイルドカード」を使える。
たとえば、2つのディレクトリから1つのタグファイルTAGS
を作るには、
2番目のディレクトリを../elisp/
とすると、
つぎのようなコマンドを入力する。
M-! etags *.el ../elisp/*.el
また
M-! etags --help
とタイプすれば、etags
が受け付けるオプションの一覧が表示される。
プログラムetags
は、Emacs Lisp、Common Lisp、Scheme、C、
Fortran、Pascal、LaTeX、ほとんどのアセンブラを扱える。
このプログラムには言語を指定するスイッチはない。
ファイル名とその内容から入力ファイルの言語を認識する。
また、自分でコードを書いているときにすでに書いてある関数を参照するときにも、
etags
はとても助けになる。
新たに関数を書き加えるごとにetags
を走らせれば、
それらはタグファイルTAGS
の一部になる。
説明した関数のうち、いくつかを簡素にまとめておく。
while
nil
を返す。
(式は、その副作用のためだけに評価される。)
たとえば、
(let ((foo 2)) (while (> foo 0) (insert (format "foo is %d.\n" foo)) (setq foo (1- foo)))) => foo is 2. foo is 1. nil
(関数insert
は、ポイント位置に引数を挿入する。
関数format
は、message
が引数を書式付けするように、
引数を書式付けした文字列を返す。
\n
は改行になる。)
re-search-forward
search-forward
のように4つの引数を取る。
nil
を返すかエラーメッセージを返すかを指定する。
省略できる。
let*
たとえば、
(let* ((foo 7) (bar (* 3 foo))) (message "`bar' is %d." bar)) => `bar' is 21.
match-beginning
looking-at
t
を返す。
eobp
t
を返す。
バッファの参照可能な部分の最後は、
ナロイングしていなければバッファの最後であり、
ナロイングしていればその部分の最後である。
prog1
たとえば、
(prog1 1 2 3 4) => 1
re-search-forward
の演習問題繰り返しと正規表現の探索は、Emacs Lispを書くときによく使う強力な道具である。
本章では、while
ループと再帰を用いた単語を数えるコマンドの
作成をとおして、正規表現の探索の使用例を示す。
Emacsの標準ディストリビューションには、 リージョン内の行数を数える関数が含まれている。 しかし、単語を数える関数はない。
ある種の文書の作成過程では、単語数を知る必要がある。
たとえば、エッセイは800語までとか、
小説を執筆するときには1日に1000語は書くことにするとかである。
Emacsに単語を数えるコマンドがないのは筆者には奇妙に思える。
たぶん、単語数を数える必要のないコードやドキュメントを書くのに
Emacsを使っているのであろう。
あるいは、オペレーティングシステムの単語を数えるコマンドwc
を
使っているのであろう。
あるいは、出版社の慣習にしたがって、文書の文字数を5で割って
単語数を計算しているのであろう。
count-words-region
単語を数えるコマンドは、行、段落、リージョン、あるいは、
バッファのなかの単語を数える。
どの範囲で数えるべきであろう?
バッファ全体で単語数を数えるようにコマンドを設計することもできるが、
Emacsの習慣では柔軟性を重んじる。
バッファ全体ではなく、ある部分の単語数を数えたい場合もある。
したがって、リージョンの中の単語数を数えるようにコマンドを
設計するほうが合理的であろう。
コマンドcount-words-region
さえあれば、
必要ならば、C-x h(mark-whole-buffer
)で
バッファ全体をリージョンとして単語を数えられる。
明らかに、単語を数えるのは繰り返し動作である。
リージョンの先頭から始めて、最初の語を数え、2番目の語を数え、3番目の語を数え
というように、リージョンの最後に達するまで繰り返す。
つまり、単語の数え上げは、再帰やwhile
ループに完璧に適しているのである。
まず、while
ループで単語を数えるコマンドを実装してから、
再帰でも書いてみる。
コマンドは、当然、対話的にする。
対話的関数の雛型はつぎのとおりである。
(defun 関数名 (引数リスト) "説明文..." (interactive-expression...) 本体...)
これらの項目を埋めればよいのである。
関数名は、十分説明的で既存の名前count-lines-region
に
似ているべきである。
こうすると、名前を覚えやすい。
count-words-region
がよいであろう。
関数はリージョン内の単語を数える。
つまり、引数リストには、リージョンの先頭の位置と最後の位置に束縛される
シンボルが含まれる必要がある。
これらの2つの位置を、それぞれ、beginning
、end
と呼ぶことにする。
apropos
などのコマンドは説明文の最初の1行しか表示しないので、
説明文の最初の1行は1つの文であるべきである。
関数の引数リストにリージョンの先頭と最後を渡す必要があるので、
interactive
式は(interactive "r")
となる。
これらは、決まりきっていることである。
関数の本体は、3つの仕事を遂行するように書く必要がある。
まず、while
ループが単語を数えられるように条件を設定し、
つぎに、while
ループを実行し、最後に、ユーザーにメッセージを送る。
ユーザーがcount-words-region
を呼ぶときには、
リージョンの先頭か最後のどちらかにポイントがある。
しかし、数え上げの処理はリージョンの先頭から始める必要がある。
つまり、ポイントが先頭になければ移動する必要がある。
(goto-char beginning)
を実行すればよい。
もちろん、関数が終了したらポイントを予想できるような位置に戻したい。
このためには、本体をsave-excursion
式で囲む必要がある。
関数本体の中心部分は、1単語分ポイントを進めて数を数える
while
ループから成る。
while
ループの判定条件は、ポイントを移動できる限りは真となり、
ポイントがリージョンの最後に達したら偽となるべきである。
単語単位にポイントを移動する式として(forward-word 1)
を
使うこともできるが、正規表現の検索を使えば
Emacsが「単語」と認識するものを容易に理解できる。
正規表現の探索では、探しあてたパターンの最後の文字の直後にポイントを置く。 つまり、単語を正しく連続して探索できるとポイントは単語単位に進むのである。
実際問題として、正規表現の探索では、単語自体だけでなく、 単語と単語のあいだの空白や句読点も飛び越してほしい。 単語と単語のあいだの空白を飛び越せないような正規表現では、 1つの単語を飛び越すこともない。 つまり、正規表現には、単語自体だけでなく、 単語に続く空白や句読点も含める必要がある (単語がバッファの最後で終わっている場合には、空白や句読点が続くことはないので、 これらに対応する正規表現の部分はなくてもよいようになっている必要がある)。
したがって、望みの正規表現は、単語を構成する1個以上の文字のあとに 単語を構成しない文字が0個以上続くようなパターンである。 このような正規表現はつぎのとおりである。
\w+\W*
どの文字が単語を構成し、どの文字が単語を構成しないかは、 バッファのシンタックステーブル(構文表)で決まる (シンタックスに関して詳しくは、 See Syntax。 あるいは、Syntax, やSyntax Tablesを参照)。
探索式はつぎのようになる。
(re-search-forward "\\w+\\W*")
(w
とW
のまえに、2つの連続したバックスラッシュがあることに
注意してほしい。
単一のバックスラッシュは、Emacs Lispインタープリタに対して特別な意味を持つ。
直後の文字を通常とは異なる意味で解釈することを指示する。
たとえば、2つの文字\n
は、バックスラッシュに続くn
ではなく、
newline
(改行)を意味する。
2つの連続したバックスラッシュは、
普通の「特別な意味のない」バックスラッシュである。)
単語が何個あったかを数えるカウンタが必要である。
この変数は、まず0に設定し、Emacsがwhile
ループを廻るごとに増やす。
増加式は簡単である。
(setq count (1+ count))
最後に、リージョン内の単語数をユーザーに伝える必要がある。
関数message
は、この種の情報をユーザーに与えるためのものである。
リージョンの単語数に関わらず、正しいメッセージである必要がある。
「there are 1 words in the region」とは表示したくない。
単数と複数の矛盾は文法的に誤りである。
この問題は、リージョン内の単語数に依存して異なるメッセージを与える条件式を
使えば解決できる。
3つの可能性がある。
リージョンには、0個の単語があるか、1個の単語があるか、
2個以上の単語があるかである。
つまり、スペシャルフォームcond
が適している。
以上により、つぎのような関数定義を得る。
;;; 第1版。バグあり! (defun count-words-region (beginning end) "Print number of words in the region. Words are defined as at least one word-constituent character followed by at least one character that is not a word-constituent. The buffer's syntax table determines which characters these are." (interactive "r") (message "Counting words in region ... ") ;;; 1. 適切な条件を設定する (save-excursion (goto-char beginning) (let ((count 0)) ;;; 2. while ループ (while (< (point) end) (re-search-forward "\\w+\\W*") (setq count (1+ count))) ;;; 3. ユーザーにメッセージを与える (cond ((zerop count) (message "The region does NOT have any words.")) ((= 1 count) (message "The region has 1 word.")) (t (message "The region has %d words." count))))))
関数はこのとおりに動作するが、正しくない場合もある。
count-words-region
count-words-region
の空白に関するバグ上の節で説明したコマンドcount-words-region
には、2つのバグ、
あるいは、2通りの現れ方をする1つのバグがある。
1つめは、文の中程にある空白のみをリージョンとすると、
コマンドcount-words-region
は単語は1個あると報告する!
2つめは、バッファの最後やナロイングしたバッファの参照可能な部分の最後にある
空白のみを含むリージョンでは、
コマンドはつぎのようなエラーメッセージを表示する。
Search failed: "\\w+\\W*"
GNU EmacsのInfoで読んでいる場合には、これらのバグを読者自身で確認できる。
まず、いつものように関数を評価してインストールする。
つぎを評価すれば、キーバインドもインストールできる。
(global-set-key "\C-c=" 'count-words-region)
第一のテストを行うには、つぎの行の先頭と末尾にマークとポイントを設定し、 C-c =(C-c =にバインドしていなければ、 M-x count-words-region)とタイプする。
one two three
Emacsは、リージョンには単語が3個あると正しく報告する。
行の先頭にマークを、単語one
の直前にポイントを置いて、
テストを繰り返してみる。
コマンドC-c =(あるいはM-x count-words-region)とタイプする。
行の始めにある空白のみなので、
リージョンには単語がないとEmacsは報告すべきである。
しかし、Emacsはリージョンには単語が1個あると報告する。
3つめのテストとしては、上の例の行をバッファ*scratch*
の最後にコピーし
行末に空白を数個タイプする。
単語three
の直後にマークを置き、行末にポイントを置く
(行末はバッファの最後でもある)。
まえと同じように、C-c =(あるいはM-x count-words-region)
とタイプする。
行末に空白のみがあるので、Emacsはリージョンに単語はないと報告すべきである。
しかし、EmacsはSearch failed
というエラーメッセージを表示する。
2つのバグは同じ問題から生じている。
バグの第一の現れ方では、行の先頭の空白を1語と数える。
これはつぎのことが起きているのである。
コマンドM-x count-words-region
は、
リージョンの先頭にポイントを移動する。
while
では、ポイントの値がend
の値より小さいかどうかを調べるが、
たしかにそのとおりである。
したがって、正規表現の探索が行われ、最初の単語を探す。
それによりポイントは単語の直後に移動する。
count
は1になる。
while
ループが繰り返されるが、ポイントの値がend
の値より
大きいのでループから抜け、
関数はリージョン内に単語が1個ある旨のメッセージを表示する。
つまり、正規表現は単語を探し出すのであるが、
その単語はリージョンの外側にあるのである。
バグの第二の現れ方では、リージョンはバッファの最後の空白である。
EmacsはSearch failed
と報告する。
while
ループの判定条件は真であるため、正規表現の探索が行われる。
しかし、バッファには単語がないので、探索に失敗する。
バグのいずれの現れ方でも、 探索の範囲がリージョンの外側にまでおよんでしまっている。
解決策は、探索をリージョン内に制限することであり、 これは比較的簡単なことであるが、しかし、思ったほどは簡単なことでもない。
すでに説明したように、関数re-search-forward
は、
探索すべきパターンを第1引数に取る。
これは必須引数であるが、これに加えて、3つの引数を取ることができる。
省略できる第2引数は、探索範囲を制限する。
省略できる第3引数にt
を指定すると、探索に失敗した場合には
エラーを通知するかわりにnil
を返す。
省略できる第4引数は、繰り返し回数である
(Emacsでは、C-h fに続けて関数名、<RET>をタイプすれば、
関数の説明文を得ることができる)。
count-words-region
の定義では、リージョンの最後は変数end
が
保持しており、これは関数への引数として渡される。
したがって、正規表現の探索式の引数にend
を追加できる。
(re-search-forward "\\w+\\W*" end)
しかし、count-words-region
の定義にこの変更だけを施して、
この新たな定義を空白だけのリージョンに対してテストすると、
Search failed
のエラーメッセージを得ることになる。
ここでは、探索はリージョン内に制限されるが、 リージョンには単語の構成文字がないので予想どおりに失敗するのである。 失敗したので、エラーメッセージを得たのである。 しかし、このような場合にエラーメッセージを得たくはなく、 「The region does NOT have any words.」のようなメッセージを得たいのである。
この問題に対する解決策は、re-search-forward
の第3引数にt
を
指定して、探索に失敗した場合にはエラーを通知するかわりにnil
を
返すようにする。
しかし、この変更を施して試してみると、メッセージ
「Counting words in region ... 」が表示され、
C-g(keyboard-quit
)をタイプするまで、
このメッセージが表示され続ける。
なにが起こっているかというと……。
まえと同じように探索はリージョン内に限られ、
リージョン内には単語を構成する文字がないので探索に失敗する。
その結果、re-search-forward
式はnil
を返す。
それ以外のことはしない。
特に、探索に成功した場合の副作用としてのポイントの移動は行われない。
re-search-forward
式がnil
を返すと、
while
ループのつぎの式が評価される。
この式はカウンタを増やす。
ついでループが繰り返される。
re-search-forward
式はポイントを移動しないので、
ポイントの値は変数end
の値より小さく、判定条件は真になる。
これが繰り返されるのである。
count-words-region
の定義をさらに変更する必要があり、
探索に失敗した場合にはwhile
ループの判定条件が偽になるようにする。
つまり、カウンタを増やすまえに判定条件で満たすべき条件が2つある。
ポイントはリージョン内にあり、かつ、探索式で単語を探し終えていることである。
第一の条件と第二の条件は同時に真である必要があるので、
2つの式、リージョンの検査と探索式を関数and
で結び、
while
ループの判定条件をつぎのようにする。
(and (< (point) end) (re-search-forward "\\w+\\W*" end t))
re-search-forward
式が真を返すのは、
探索に成功し、かつ、副作用としてポイントを移動した場合である。
つまり、単語を探し出すと、リージョン内でポイントが移動する。
別の単語を探すのに失敗したり、リージョンの最後にポイントが達すると、
判定条件は偽になり、while
ループを抜け出し、
関数count-words-region
はメッセージの1つを表示する。
これらの最終的な変更を施すと、count-words-region
はバグなしに
(あるいは、少なくとも、筆者にはバグのない)動作をする。
つぎのとおりである。
;;; 最終版 while
(defun count-words-region (beginning end)
"Print number of words in the region."
(interactive "r")
(message "Counting words in region ... ")
;;; 1. 適切な条件を設定する
(save-excursion
(let ((count 0))
(goto-char beginning)
;;; 2. while ループ
(while (and (< (point) end)
(re-search-forward "\\w+\\W*" end t))
(setq count (1+ count)))
;;; 3. ユーザーにメッセージを与える
(cond ((zerop count)
(message
"The region does NOT have any words."))
((= 1 count)
(message
"The region has 1 word."))
(t
(message
"The region has %d words." count))))))
while
ループのかわりに再帰的に単語を数え上げる関数を書くこともできる。
どのようにするかを説明しよう。
まず、関数count-words-region
が3つの処理を行うことを認識する必要がある。
数え上げのための適切な条件を設定する、
リージョン内の単語を数え上げる、
単語の個数をユーザーに伝えるメッセージを送るである。
すべてを行う単一の再帰的関数を書くと、再帰呼び出しごとにメッセージを 受け取ることになる。 たとえば、リージョンに13個の単語があった場合、順番に13個のメッセージを得る。 こうはしたくない。 かわりに、それぞれの処理を行う2つの関数を書き、 一方(再帰的関数)を他方の内部で使う。 一方の関数で条件を設定してメッセージを表示し、他方は数え上げた単語数を返す。
メッセージを表示する関数から始めよう。
これもcount-words-region
と呼ぶことにする。
ユーザーが呼び出すのは、この関数である。
これは対話的にする。
もちろん、これはまえの版の関数に似ているが、
リージョン内の単語を数えるためにrecursive-count-words
を呼び出す。
まえの版をもとにして、この関数の雛型を作ることができる。
;; 再帰版;正規表現の探索を用いる (defun count-words-region (beginning end) "説明文..." (interactive-expression...) ;;; 1. 適切な条件を設定する (説明メッセージを表示) (関数を設定する... ;;; 2. 単語を数える 再帰呼び出し ;;; 3. ユーザーにメッセージを与える 単語数を与えるメッセージ))
定義は単純であるが、再帰呼び出しで得られた単語数をどうにかして
表示メッセージに渡す必要がある。
少し考えれば、let
式を使えばよいことがわかる。
let
式の変数リストにて、再帰呼び出しで得られたリージョン内の
単語数を変数に束縛し、この束縛を使ってcond
式でユーザーに値を表示する。
しばしば、let
式内の束縛は、関数の「主要」な処理に対して
副次的なものと考えられる。
しかし、この場合には、関数の「主要」な処理、つまり単語を数えることが、
let
式の内側で行われていると考えられる。
let
を使うと、関数定義はつぎのようになる。
(defun count-words-region (beginning end) "Print number of words in the region." (interactive "r") ;;; 1. 適切な条件を設定する (message "Counting words in region ... ") (save-excursion (goto-char beginning) ;;; 2. 単語を数える (let ((count (recursive-count-words end))) ;;; 3. ユーザーにメッセージを与える (cond ((zerop count) (message "The region does NOT have any words.")) ((= 1 count) (message "The region has 1 word.")) (t (message "The region has %d words." count))))))
つぎは、再帰的に数え上げる関数を書くことである。
再帰関数には少なくとも3つの部分が必要である。 つまり、「再帰条件」、「次段式」、再帰呼び出しである。
再帰条件は、関数が再度呼び出しを行うかどうかを決定する。
リージョン内の単語を数えるのであり、
また、単語ごとにポイントを進めるために関数を
使えるので、再帰条件ではポイントがリージョン内にあるかどうかを調べる。
再帰条件では、ポイントの値を調べて、リージョンの最後のまえか、
最後か、そのうしろかを決定する。
ポイントの位置を知るには関数point
を使う。
明らかに、再帰的に単語を数える関数の引数には、
リージョンの最後を渡す必要がある。
さらに、再帰条件では、単語を探せたかどうかを調べるべきである。 みつからなかった場合には、関数は自身を再度呼び出すべきではない。
次段式は、再帰関数が自身の呼び出しを止めるべきときに止めるように値を変更する。 より正確には、次段式は、再帰条件が再帰関数の自身の呼び出しを止めるように 値を変更する。 この場合、次段式はポイントを1単語分進める式である。
再帰関数の3番目の部分は、再帰呼び出しである。
関数の処理、つまり、数え上げを行う部分も必要である。 これは本質的な部分である!
再帰的に数え上げる関数の概略はすでに説明してある。
(defun recursive-count-words (region-end) "説明分..." 再帰条件 次段式 再帰呼び出し)
これらの項目を埋めればよい。 もっとも簡単な部分から始めよう。 ポイントがリージョンの最後か最後を越えていれば、 リージョン内に単語があるはずはないので、関数は0を返すべきである。 同様に、探索に失敗した場合にも、 数えるべき単語はないので、関数は0を返すべきである。
一方、ポイントがリージョン内にあり、探索に成功した場合には、 関数は自身を再度呼び出す必要がある。
したがって、再帰条件はつぎのようになる。
(and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t))
探索式は再帰条件の一部であることに注意してほしい。
探索に成功するとt
を返し、失敗するとnil
を返す
(re-search-forward
の動作の説明は、
See Whitespace Bug)。
再帰条件は、if
節の判定条件である。
明らかに、再帰条件が真ならば、if
節の真の場合の動作では関数を呼び出す。
偽ならば、ポイントがリージョンの外側にあるか、
探すべき単語がなくて探索に失敗するので
偽の場合の動作では0を返すべきである。
しかし、再帰呼び出しを考えるまえに、次段式を考える必要がある。 どうすべきだろう? 興味深いことに、次段式は再帰条件の探索部分である。
再帰条件にt
やnil
を返すことに加えて、
re-search-forward
は、探索に成功したときの副作用としてポイントを進める。
この動作は、ポイントがリージョンの最後に達した場合に、
再帰関数が自身の呼び出しを止めるようにポイントの値を変更する。
つまり、re-search-forward
式は次段式でもある。
したがって、関数recursive-count-words
の概略はつぎのようになる。
(if 再帰条件と次段式 ;; 真の場合の動作 個数を返す再帰呼び出し ;; 偽の場合の動作 0を返す)
数え上げる機構をどのように組み込むか?
再帰関数を書き慣れていないと、このような問いかけは混乱のもとかもしれない。 しかし、系統的に扱う必要があり、また、そうすべきである。
数え上げる機構は、再帰呼び出しに関連付けられるべきであることは知っている。
次段式はポイントを1単語分先へ進め、各単語に対して再帰呼び出しが行われるので、
数え上げる機構は、recursive-count-words
を呼び出して返された値に
1を加える式である必要がある。
いくつかの場合を考えてみよう。
以上のスケッチから、if
の偽の場合の動作では、単語がなければ0を
返すことがわかる。
また、if
の真の場合の動作では、残りの単語を数えて返された値に
1を加えた値を返す必要がある。
引数に1を加える関数を1+
とすれば、式はつぎのようになる。
(1+ (recursive-count-words region-end))
recursive-count-words
関数の全体はつぎのようになる。
(defun recursive-count-words (region-end) "説明文..." ;;; 1. 再帰条件 (if (and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t)) ;;; 2. 真の場合の動作:再帰呼び出し (1+ (recursive-count-words region-end)) ;;; 3. 偽の場合の動作 0))
これがどのように動作するか説明しよう。
リージョン内に単語がなければ、if
式の偽の場合の動作が評価され、
その結果、関数は0を返す。
リージョン内に単語が1つある場合、
ポイントの値はregion-end
の値よりも小さく、探索に成功する。
この場合、if
の判定条件は真を返し、真の場合の動作が評価される。
数え上げる式が評価される。
この式は再帰呼び出しが返す値に1を加えた(関数全体の値として返される)値を返す。
同時に、次段式はリージョン内の最初の(この場合は唯一の)
単語を越えてポイントを移動する。
つまり、(recursive-count-words region-end)
が、
再帰呼び出しの結果として2回目に評価されると、
ポイントの値はリージョンの最後に等しいか大きい。
したがって、そのとき、recursive-count-words
は0を返す。
0を1に加えるので、recursive-count-words
のもとの評価値は1足す0、つまり、
1を返し、これは正しい値である。
明らかに、リージョン内に2つの単語がある場合、
recursive-count-words
の始めの呼び出しは、
リージョン内の残りの単語に対して呼び出したrecursive-count-words
が返す値に1を加えた値を返す。
つまり、1足す1は2であり、これは正しい値である。
同様に、リージョン内に3つの単語がある場合には、
recursive-count-words
の最初の呼び出しは、
リージョン内の残りの2つの単語に対して呼び出したrecursive-count-words
が返す値に1を加えた値を返す。
説明文を加えると、2つの関数はつぎのようになる。
再帰関数:
(defun recursive-count-words (region-end) "Number of words between point and REGION-END." ;;; 1. 再帰条件 (if (and (< (point) region-end) (re-search-forward "\\w+\\W*" region-end t)) ;;; 2. 真の場合の動作:再帰呼び出し (1+ (recursive-count-words region-end)) ;;; 3. 偽の場合の動作 0))
呼び出し側:
;;; 再帰版 (defun count-words-region (beginning end) "Print number of words in the region. Words are defined as at least one word-constituent character followed by at least one character that is not a word-constituent. The buffer's syntax table determines which characters these are." (interactive "r") (message "Counting words in region ... ") (save-excursion (goto-char beginning) (let ((count (recursive-count-words end))) (cond ((zerop count) (message "The region does NOT have any words.")) ((= 1 count) (message "The region has 1 word.")) (t (message "The region has %d words." count))))))
while
を使って、リージョン内の句読点、ピリオド、カンマ、セミコロン、
コロン、感嘆符、疑問符を数える関数を書いてみよ。
また、再帰を使って書いてみよ。
defun
内の単語の数え上げつぎの目標は、関数定義内の語数を数えることである。
明らかに、count-words-region
の変形を使えばできる。
See Counting Words。
1つの定義内の単語を数えるだけならば、
定義をコマンドC-M-h(mark-defun
)でマークして、
count-words-region
を呼べばよい。
しかし、より野心的でありたい。 Emacsソース内の各定義ごとに単語やシンボルを数え、それぞれの数ごとに いくつの関数があるかを表すグラフを書きたい。 40から49個の単語やシンボルを含む関数はいくつ、 50から59個の単語やシンボルを含む関数はいくつなどを表示したいのである。 筆者は、典型的な関数定義の長さに興味を持っており、この関数でそれがわかる。
count-words
.
一言でいえば、目標はヒストグラムを作ることである。 多数の小さな部分に分けて一度に1つずつ扱えば、恐れることはない。 どのような手順を踏めばよいか考えよう。
count-words-in-defun
を使う。
これはとても手応えのあることである。 しかし、順を追って進めれば、困難ではない。
関数定義内の単語を数えることを考えた場合、
最初の疑問は何を数えるのかということである。
Lispの関数定義に関して「単語」といえば、ほとんどの場合、
「シンボル」のことである。
たとえば、つぎの関数multiply-by-seven
には、
defun
、multiply-by-seven
、number
、
*
、7
の5つのシンボルがある。
さらに、4つの単語、Multiply
、NUMBER
、by
、seven
を
含んだ説明文字列がある。
シンボルnumber
は繰り返し使われているので、
定義には全部で10個の単語とシンボルが含まれる。
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number))
しかし、multiply-by-seven
の定義にC-M-h(mark-defun
)で
マークしてからcount-words-region
を呼ぶと、
count-words-region
は定義には10ではなく11語があると報告する!
何かがおかしい!
問題は2つあり、count-words-region
は*
を単語として数えないことと、
単一のシンボルmultiply-by-seven
を3語として数えることである。
ハイフンを単語を繋ぐ文字としてではなく、単語のあいだの空白として扱う。
multiply-by-seven
は、
multiply by seven
と書かれているかのように数えられる。
このような混乱の原因は、count-words-region
の定義のなかの
ポイントを単語単位に進める正規表現の探索にある。
count-words-region
の正規表現はつぎのとおりである。
"\\w+\\W*"
この正規表現は、単語の構成文字の1個以上の繰り返しに 単語を構成しない文字が0個以上繰り返したものを続けたパターンである。 つまり、「単語の構成文字」ということでシンタックスの問題になるのであり、 その重要性から1つの節で扱おう。
Emacsは、それぞれの文字をそれぞれが属する
シンタックスカテゴリ(syntax categories)に応じて扱う。
たとえば、正規表現\\w+
は、単語構成文字の1個以上の繰り返しを
意味するパターンである。
単語構成文字は、1つのシンタックスカテゴリである。
他のシンタックスカテゴリには、ピリオドやカンマなどの句読点文字のクラスや、
空白文字やタブ文字などの空白文字クラスがある
(より詳しくは、SyntaxやSyntax Tablesを参照)。
シンタックステーブルは、どの文字がどのカテゴリに属するかを指定する。
普通、ハイフンは「単語構成文字」ではない。
かわりに、「シンボルの一部ではあるが単語の一部ではないクラス」に指定される。
つまり、関数count-words-region
は、ハイフンを単語のあいだの
空白と同様に扱い、そのためcount-words-region
は
multiply-by-seven
を3語と数えるのである。
Emacsにmultiply-by-seven
を1つのシンボルとして数えさせるには、
2つの方法がある。
シンタックステーブルを変更するか、正規表現を変更するかである。
Emacsが各モードごとに保持するシンタックステーブルを変更して、 ハイフンを単語構成文字として再定義することもできる。 この方法でもわれわれの目的は達せられるが、 ハイフンはシンボルの一部になるもっとも一般的な文字であるが、 典型的な単語構成文字ではない。 このような文字は他にもある。
かわりに、count-words-region
の定義の中の正規表現を
シンボルを含むように再定義することである。
この方法は明確ではあるが、技巧を要する。
最初の部分は簡単で、パターンは「単語やシンボルを構成する少なくとも1文字」 である。 つまり、つぎのようになる。
\\(\\w\\|\\s_\\)+
\\(
は、\\|
で区切られた\\w
と代替の\\s_
を
含むグループ構成の開始部分である。
\\w
は任意の単語構成文字に一致し、
\\s_
は単語構成文字ではないがシンボル名の一部に
成りえる任意の文字に一致する。
グループに続く+
は、単語やシンボルを構成する文字が少なくとも1つ
あることを意味する。
しかし、正規表現の第二の部分は、設計がより困難である。 最初の部分に続けて、「単語やシンボルを構成しない文字が0個以上続く」と 指定したいのである。 まず、筆者は、つぎのような定義を考えた。
\\(\\W\\|\\S_\\)*"
大文字のW
やS
は、単語やシンボルを構成しない文字に一致する。
残念ながら、この式は、単語を構成しない任意の文字かシンボルを構成しない
任意の文字に一致する。
つまり、任意の文字に一致するのである!
筆者が試したリージョンでは、単語やシンボルには空白文字 (空白、タブ、改行)が続いていることに気づいた。 そこで、単語やシンボルを構成する文字の1個以上の繰り返しパターンのあとに 1個以上の空白文字が続くパターンを試してみた。 これも失敗であった。 単語やシンボルはしばしば空白で区切られるが、実際のコードでは、 シンボルのあとには括弧が、単語のあとには句読点が続く。 結局、単語やシンボルを構成する文字に続いて空白文字以外の文字が0個以上続き、 さらに、空白文字が0個以上続くパターンにした。
全体の正規表現はつぎのとおりである。
"\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*"
count-words-in-defun
関数count-words-region
の書き方には、何通りかあることを説明した。
count-words-in-defun
を書くには、それらの1つを適用すればよい。
while
ループを使った版は、容易に理解できるので、
これを適用することにする。
count-words-in-defun
は、複雑なプログラムの一部となるので、
対話的である必要はなく、メッセージを表示する必要もなく、
単に語数を返せばよい。
このため、定義は少々簡単になる。
一方、count-words-in-defun
は、関数定義を含んだバッファで利用される。
つまり、関数定義内にポイントがある状態で呼ばれたかどうかを調べ、
もしそうなら、その定義の語数を返すようにするのが合理的であろう。
こうすると定義に余分な複雑さを伴うが、関数に引数を渡さないですむ。
以上から、関数の雛型はつぎのようになる。
(defun count-words-in-defun () "説明文..." (初期設定... (whileループ...) 語数を返す)
いつものように、各項目を埋めていく。
まずは、初期設定である。
この関数は、関数定義を含むバッファで呼ばれると仮定している。
ポイントは、関数定義の内側か外側にある。
count-words-in-defun
が動作するには、ポイントを定義の先頭に移動し、
カウンタを0で始め、ポイントが定義の最後に達したらループを終了する。
関数beginning-of-defun
は、行の先頭にある(
などの
開き区切り記号を逆向きに探し、ポイントをそこへ移動する。
あるいは、探索範囲内で止まる。
実際、beginning-of-defun
は、ポイントを囲んでいる関数定義の先頭か
直前の関数定義の先頭にポイントを移動するか、バッファの先頭に移動する。
ポイントを開始場所へ移動するにはbeginning-of-defun
を使えばよい。
while
ループには、単語やシンボルを数えるカウンタが必要である。
let
式で、このためのローカル変数を作って、初期値0に束縛する。
関数end-of-defun
は、beginning-of-defun
と同じように動作するが、
関数定義の最後にポイントを移動する点が異なる。
end-of-defun
は、関数定義の最後に達したかどうかを調べる式に使える。
count-words-in-defun
の初期設定はつぎのようになる。
まず、ポイントを関数定義の先頭に移動し、
語数を保持するローカル変数を作り、
最後に、while
ループがループの終了を判定できるように
関数定義の終わりの位置を記録する。
コードはつぎのようになる。
(beginning-of-defun) (let ((count 0) (end (save-excursion (end-of-defun) (point))))
コードは簡単である。
少々複雑な部分はend
に関する部分であろう。
save-excursion
式を使って、end-of-defun
で一時的に
定義の最後にポイントを移動してからポイントの値を返すことで、
定義の最後の位置を変数に束縛する。
count-words-in-defun
の初期設定後の2番目の部分は、
while
ループである。
ループには、語単位やシンボル単位でポインタを進める式や、
語数を数える式が必要である。
while
の判定条件は、ポイントを進められた場合には真を、
ポイントが定義の最後に達した場合には偽を返す必要がある。
これ(see Syntax)
に必要な正規表現はすでにわかっているので、ループは簡単である。
(while (and (< (point) end) (re-search-forward "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t) (setq count (1+ count)))
関数定義の3番目の部分では、単語やシンボルの個数を返す。
この部分は、let
式の本体内の最後の式で、
非常に簡単な式、つまり、ローカル変数count
であり、
評価されると語数を返す。
以上をまとめると、count-words-in-defun
の定義はつぎのようになる。
(defun count-words-in-defun () "Return the number of words and symbols in a defun." (beginning-of-defun) (let ((count 0) (end (save-excursion (end-of-defun) (point)))) (while (and (< (point) end) (re-search-forward "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t)) (setq count (1+ count))) count))
これをどのようにテストしようか?
関数は対話的ではないが、対話的に呼び出すための呼び出し関数を
作るのは簡単である。
count-words-region
の再帰版のようなコードを使えばよい。
;;; 対話的な版 (defun count-words-defun () "Number of words and symbols in a function definition." (interactive) (message "Counting words and symbols in function definition ... ") (let ((count (count-words-in-defun))) (cond ((zerop count) (message "The definition does NOT have any words or symbols.")) ((= 1 count) (message "The definition has 1 word or symbol.")) (t (message "The definition has %d words or symbols." count)))))
C-c =をキーバインドとして再利用しよう。
(global-set-key "\C-c=" 'count-words-defun)
これで、count-words-defun
を試せる。
count-words-in-defun
とcount-words-defun
をインストールし、
キーバインドを設定してから、つぎの関数定義にカーソルを置く。
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number)) => 10
うまくいった! 定義には10個の単語やシンボルがある。
つぎの問題は、1つのファイルの中にある複数の定義の中の単語やシンボルの 個数を数えることである。
defuns
の数え上げsimple.el
のようなファイルには80以上もの関数定義が含まれる。
われわれの最終目標は、複数のファイルについて統計を集めることであるが、
第1段階としての中間目標は、1つのファイルについて統計を集めることである。
この情報は、関数定義の長さを表す数を並べたものになる。 これらの数は、リストに収めることができる。
1つのファイルから得た情報を他の多くのファイルから得た情報に 加える必要があるので、この関数では、1つのファイル内の定義を 数えた数をリストにして返す必要がある。 この関数は、いかなるメッセージも表示する必要はなく、また、してはならない。
語数を数えるコマンドには、語単位にポイントを進める式と、 語数を数える式が含まれる。 関数定義の長さを数える関数も同じように、 定義単位にポイントを進める式と長さのリストを作る式で 動作するように設計できる。
問題をこのように文書にすることは、関数定義を書く基本である。
ファイルの先頭から数え始めるので、最初のコマンドは
(goto-char (point-min))
になる。
つぎに、while
ループを始めるのであるが、
ループの判定条件は、つぎの関数定義を探す正規表現の探索式である。
探索に成功する限りポイントを先に進め、ループの本体を評価する。
本体には、長さのリストを作る式が必要である。
リスト作成のコマンドcons
を使ってリストを作る。
これでほとんどできあがりである。
コードの断片を示そう。
(goto-char (point-min)) (while (re-search-forward "^(defun" nil t) (setq lengths-list (cons (count-words-in-defun) lengths-list)))
残っているのは、関数定義を収めたファイルを探す機構である。
まえの例では、Infoファイルやバッファ*scratch*
などの他のバッファに
切り替えていた。
ファイルを探すという動作は、まだ説明していない新しい動作である。
Emacsでファイルを探すには、コマンドC-x C-f(find-file
)を使う。
このコマンドは、われわれの長さの問題にはほぼよいのではあるが、
ぴったりではない。
find-file
のソースを見てみよう
(関数のソースを探すにはコマンドfind-tag
を使う)。
(defun find-file (filename) "Edit file FILENAME. Switch to a buffer visiting file FILENAME, creating one if none already exists." (interactive "FFind file: ") (switch-to-buffer (find-file-noselect filename)))
定義には短いが完全な説明文があり、
コマンドを対話的に使った場合にファイル名を
問い合わせるinteractive
式もある。
定義の本体には、2つの関数、find-file-noselect
と
switch-to-buffer
がある。
C-h f(コマンドdescribe-function
)で表示される説明文によれば、
関数find-file-noselect
は、指定されたファイルをバッファに読み込み、
そのバッファを返す。
しかし、バッファを選択することはしない。
(find-file-noselect
を使った場合、)
Emacsは指定したバッファに注意を向けないのである。
これはswitch-to-buffer
が行うことであり、
Emacsの注意を指定したバッファに向け、そのバッファをウィンドウに表示する。
バッファの切り替えについてはすでに説明した
(See Switching Buffers)。
われわれのヒストグラムの計画では、
プログラムが各定義の長さを調べる個々のファイルを表示する必要はない。
switch-to-buffer
を使うかわりに、
set-buffer
を使って、プログラムの注意を別のバッファに向けるが、
画面には表示しない。
したがって、find-file
を呼んで操作するかわりに、
独自の式を書く必要がある。
これは簡単であり、find-file-noselect
とset-buffer
を使う。
lengths-list-file
の詳細関数lengths-list-file
の核は、defun単位にポイントを移動する機能と、
各defun内の単語やシンボルを数える機能である。
この核を、ファイルを探したり、ファイルの先頭にポイントを移動するなどの
さまざまな処理を行う機能で囲む。
関数定義はつぎのようになる。
(defun lengths-list-file (filename) "Return list of definitions' lengths within FILE. The returned list is a list of numbers. Each number is the number of words or symbols in one function definition." (message "Working on `%s' ... " filename) (save-excursion (let ((buffer (find-file-noselect filename)) (lengths-list)) (set-buffer buffer) (setq buffer-read-only t) (widen) (goto-char (point-min)) (while (re-search-forward "^(defun" nil t) (setq lengths-list (cons (count-words-in-defun) lengths-list))) (kill-buffer buffer) lengths-list)))
関数には1つの引数、処理すべきファイル名を渡す。
4行の説明文があるが、interactive
式はない。
何も表示されないとコンピュータが壊れたと心配する人がいるので、
本体の最初の行でメッセージを表示する。
つぎの行には、save-excursion
があり、関数が終了したときに
Emacsの注意をカレントバッファに戻す。
これは、もとのバッファにポイントが戻ると仮定している関数から
この関数を使う場合に便利である。
let
式の変数リストでは、Emacsが探したファイルを含んだバッファに
ローカル変数buffer
を束縛する。
同時に、Emacsはローカル変数としてlengths-list
を作る。
続いて、Emacsはバッファに注意を向ける。
続く行では、Emacsはバッファを読み出し専用にする。 理想的にはこの行は必要ない。 関数定義内の単語やシンボルを数える関数は、バッファを変更しない。 さらに、バッファを変更したとしてもバッファを保存しない。 この行は、最大限の注意を払ったものである。 注意する理由は、この関数やこれが呼び出す関数はEmacsのソースを処理するので、 それらが不注意に変更してしまうと、とても都合が悪い。 テストに失敗してEmacsのソースファイルを変更してしまうまでは、 筆者もこの行の必要性を認識していなかった。
つぎに続くのは、バッファがナロイングされている場合に備えたワイドニングである。 この関数も普通は必要ない。 バッファが既存でなければEmacsは新たにバッファを作成する。 しかし、ファイルを訪問しているバッファが既存ならば、 Emacsはそのバッファを返す。 この場合には、バッファがナロイングされている可能性があるので ワイドニングする。 完全に「ユーザーフレンドリ」を目指すならば、ナロイングやポイントの位置を 保存するべきであるが、ここではしていない。
式(goto-char (point-min))
は、ポイントをバッファの先頭へ移動する。
つぎに続くのは、この関数の処理を行うwhile
ループである。
このループでは、Emacsは、各定義の長さを調べ、情報を収めた長さのリストを作る。
処理を終えると、Emacsはバッファをキルする。
これは、Emacsが使用するメモリを節約するためである。
筆者のEmacs第19版には、調べたいファイルが300以上もある。
別の関数で、各ファイルにlengths-list-file
を適用する。
Emacsがすべてのソースファイルを訪問して1つも削除しないと、
筆者のコンピュータの仮想メモリを使い切ってしまう。
let
式の最後の式は、変数lengths-list
であり、
関数全体の値として返される値である。
いつものようにこの関数をインストールすれば、試せる。
つぎの式の直後にカーソルを置いてC-x C-e(eval-last-sexp
)と
タイプする。
(lengths-list-file "../lisp/debug.el")
(ファイルのパス名を変更する必要があろう。
InfoファイルとEmacsのソースが
/usr/local/emacs/info
と/usr/local/emacs/lisp
のように
隣り合っていれば、このパスでよい。
式を変更するには、バッファ*scratch*
にこの式をコピーして、
それを修正する。
そして、それを評価する。)
筆者のEmacsの版では、debug.el
の長さのリストを調べるのに
7秒かかり、つぎのような結果を得た。
(75 41 80 62 20 45 44 68 45 12 34 235)
ファイルの最後にある定義の長さがリストの先頭にあることに注意してほしい。
defuns
内の単語の数え上げ前節では、ファイル内の各定義の長さをリストにして返す関数を作成した。 ここでは、複数のファイルの各定義の長さのリストを返す関数を定義する。
ファイルの並びのおのおのを処理することは繰り返し動作であるので、
while
ループか再帰を使える。
while
ループを使った設計は、決まりきっている。
関数に渡す引数はファイルのリストである。
すでに見てきたように(see Loop Example)、
リストに要素がある限りループの本体を評価し、
リストが空になったらループを抜け出るように
while
ループを書くことができる。
これが動作するためには、本体を評価するたびにリストを短くし、
最終的にはリストが空になるような式がループの本体に含まれる必要がある。
雛型はつぎのようになる。
(while リストが空かどうか調べる 本体... リストにリストのcdrを設定)
while
ループは(判定条件を評価した結果である)nil
を返し、
本体の評価結果を返さない
(ループの本体内の式は、副作用のために評価される)。
しかし、長さのリストを設定する式は本体の中にあり、
その値を関数全体の値としてほしいのである。
これには、while
ループをlet
式で囲み、
let
式の最後の要素が長さのリストの値を含むようにする
(See Incrementing Example)。
以上のことを考慮すると、関数はつぎのようになる。
;;; while
ループを使う
(defun lengths-list-many-files (list-of-files)
"Return list of lengths of defuns in LIST-OF-FILES."
(let (lengths-list)
;;; 判定条件
(while list-of-files
(setq lengths-list
(append
lengths-list
;;; 長さのリストを作る
(lengths-list-file
(expand-file-name (car list-of-files)))))
;;; ファイルのリストを短くする
(setq list-of-files (cdr list-of-files)))
;;; 長さのリストを最終的な値として返す
lengths-list))
expand-file-name
は組み込み関数であり、
ファイル名を絶対パス名に変換する。
したがって、
debug.el
は、つぎのようになる。
/usr/local/emacs/lisp/debug.el
この関数定義の中で説明していないものは関数append
であるが、
次節で説明する。
append
関数append
は、リストを他のリストに繋げる。
たとえば、
(append '(1 2 3 4) '(5 6 7 8))
は、つぎのリストを作り出す。
(1 2 3 4 5 6 7 8)
lengths-list-file
を呼び出して得た2つのリストを、
このように繋ぎたい。
結果は、cons
と対照的である。
(cons '(1 2 3 4) '(5 6 7 8))
では、cons
の第1引数が新たなリストの第1要素になる。
((1 2 3 4) 5 6 7 8)
while
ループのかわりに、
ファイルのリストのおのおのを再帰的に処理してもできる。
lengths-list-many-files
の再帰版は、短くて簡単である。
再帰的関数には、「再帰条件」、「次段式」、再帰呼び出しの部分がある。
「再帰条件」は、関数が再度自身を呼び出すかどうかを決めるもので、
list-of-files
に要素があれば自身を呼び出す。
「次段式」はlist-of-files
をそのCDRに設定し直し、
最終的にはリストが空になるようにする。
再帰呼び出しでは、短くしたリストに対して自身を呼び出す。
関数全体は、この説明よりも短い!
(defun recursive-lengths-list-many-files (list-of-files) "Return list of lengths of each defun in LIST-OF-FILES." (if list-of-files ; 再帰条件 (append (lengths-list-file (expand-file-name (car list-of-files))) (recursive-lengths-list-many-files (cdr list-of-files)))))
関数は、list-of-files
の最初の長さのリストを、
list-of-files
の残りに対して自身を呼び出した結果に繋ぎ、
それを返す。
各ファイルに個別にlengths-list-file
を適用した結果を添えて、
recursive-lengths-list-many-files
の実行結果を示そう。
recursive-lengths-list-many-files
とlengths-list-file
を
インストールしてから、つぎの式を評価する。
ファイルのパス名は変更する必要があるだろう。
InfoファイルとEmacsのソースがデフォルトの場所にあれば、
変更する必要はない。
式を変更するには、これらをバッファ*scratch*
にコピーして、
そこで変更して評価する。
結果は=>
のあとに記した
(これらの値はEmacs第18.57版のファイルに対するものである。
Emacsの版が異なれば、結果も異なる)。
(lengths-list-file "../lisp/macros.el") => (176 154 86) (lengths-list-file "../lisp/mailalias.el") => (116 122 265) (lengths-list-file "../lisp/makesum.el") => (85 179) (recursive-lengths-list-many-files '("../lisp/macros.el" "../lisp/mailalias.el" "../lisp/makesum.el")) => (176 154 86 116 122 265 85 179)
関数recursive-lengths-list-many-files
は、望みの結果を出している。
つぎの段階は、グラフ表示するためにリスト内のデータを準備することである。
関数recursive-lengths-list-many-files
は、個数のリストを返す。
各個数は、関数定義の長さである。
このデータを、グラフ作成に適した数のリストに変換したいのである。
新しいリストでは、単語やシンボルが10個未満の関数定義はいくつ、
10から19個のものはいくつ、20から29個のものはいくつ、などとしたい。
つまり、関数recursive-lengths-list-many-files
が作成した
長さのリスト全体を処理して、長さの範囲ごとに数を数えて、
これらの数から成るリストを作りたいのである。
これまでの知識をもとにすれば、 長さのリストの「CDRs」を辿りながら、各要素を調べ、 どの長さの範囲に含まれるかを決めて、その範囲のカウンタを増やす関数を 書くことは難しくないことがわかる。
しかし、そのような関数を書き始めるまえに、 長さのリストを最小数から最大数の順にソートした場合の利点を考えてみよう。 まず、ソートしてあると、2つの並んだ数は同じ範囲か隣り合う範囲にあるので、 各範囲の数を数えるのが簡単になる。 第二に、ソートされたリストを調べれば、最大数と最小数がわかるので、 最大の範囲と最小の範囲を決定できる。
Emacsにはリストをソートする関数sort
がある。
関数sort
は2つの引数、ソートすべきリストと、
リストの2つの要素のうち最初のものが2番目より「小さい」かどうかを
調べる述語を取る。
すでに説明したように
(see Wrong Type of Argument)
述語とは、ある性質が真か偽かを調べる関数である。
関数sort
は、述語にしたがって、リストの順番を変える。
つまり、sort
は、非数値的なリストを非数値的な条件で
ソートする場合にも使え、たとえば、リストをアルファベット順にもできる。
数値のリストをソートするときには、関数<
を使う。
たとえば、
(sort '(4 8 21 17 33 7 21 7) '<)
は、つぎのようになる。
(4 7 7 8 17 21 21 33)
(この例では、sort
に引数として渡すまえにシンボルを評価したくないので、
どちらの引数もクオートした。)
関数recursive-lengths-list-many-files
が返したリストを
ソートするのは簡単である。
(sort (recursive-lengths-list-many-files '("../lisp/macros.el" "../lisp/mailalias.el" "../lisp/makesum.el")) '<)
これは、つぎのようになる。
(85 86 116 122 154 176 179 265)
(この例では、sort
に渡すリストを生成するために
式を評価する必要があるので、sort
の第1引数はクオートしない。)
関数recursive-lengths-list-many-files
は、
引数としてファイルのリストを必要とする。
上の例では、リストを手で書いた。
しかし、Emacs Lispのソースディレクトリは、こうするには大きすぎる。
かわりに、リストを作るために関数directory-files
を使う必要がある。
関数directory-files
は、3つの引数を取る。
第1引数は、ディレクトリ名を表す文字列である。
第2引数にnil
以外の値を指定すると、
関数はファイルの絶対パス名を返す。
第3引数は選択子である。
これに(nil
ではなく)正規表現を指定すると、
この正規表現に一致するパス名のみを返す。
したがって、筆者のシステムで、
(length (directory-files "../lisp" t "\\.el$"))
とすると、筆者の第19.25版のLispのソースディレクトリには307個の
.el
ファイルがあることがわかる。
recursive-lengths-list-many-files
が返したリストをソートする式は
つぎのようになる。
(sort (recursive-lengths-list-many-files (directory-files "../lisp" t "\\.el$")) '<)
われわれの中間目標は、単語やシンボルが10個未満の関数定義はいくつ、
10から19個のものはいくつ、20から29個のものはいくつ、などなどを表す
リストを生成することである。
ソートした数のリストでは、この処理は簡単である。
まず、10未満の要素数を数え、数え終わった数を飛び越えてから、
20未満の要素数を数え、数え終わった数を飛び越えてから、
30未満の要素数を数え、とすればよい。
10、20、30、40などの各数は、各範囲の最大の数より1大きい。
これらの数のリストをリストtop-of-ranges
としよう。
このリストを自動的に生成することも可能であるが、 書き下したほうが簡単である。 つぎのようになる。
(defvar top-of-ranges '(10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300) "List specifying ranges for `defuns-per-range'.")
範囲を変更するには、このリストを修正する。
つぎは、各範囲にある定義の数のリストを作る関数を書くことである。
明らかに、この関数は、引数としてsorted-lengths
と
リストtop-of-ranges
を取る必要がある。
関数defuns-per-range
は、2つのことを繰り返し行う必要がある。
現在の最大値で指定される範囲内にある定義の数を数えることと、
その範囲内の定義の個数を数え終えたらリストtop-of-ranges
のつぎの値に
シフトすることである。
これらの各操作は繰り返しであるので、while
ループを使ってできる。
1つのループで指定された範囲の定義の個数を数え、
もう一方のループで順番にリストtop-of-ranges
のつぎの値を選ぶ。
sorted-lengths
の数個の要素を各範囲において数える。
つまり、小さな歯車が大きな歯車の内側にあるように、
リストsorted-lengths
を処理するループは、
リストtop-of-ranges
を処理するループの内側になる。
内側のループでは、各範囲内の定義の個数を数える。
これは、すでに見てきたような簡単な数え上げのループである
(See Incrementing Loop)。
ループの判定条件では、リストsorted-lengths
の値が範囲の最大値より
小さいかどうかを調べる。
そうならば、関数はカウンタを増やし、
リストsorted-lengths
のつぎの値を調べる。
内側のループはつぎのようになる。
(while 長さの要素が範囲の最大値より小さい (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths)))
外側のループはリストtop-of-ranges
の最小値から始め、
順番につぎに大きい値に設定する。
これはつぎのようなループで行う。
(while top-of-ranges ループの本体... (setq top-of-ranges (cdr top-of-ranges)))
まとめると、2つのループはつぎのようになる。
(while top-of-ranges ;; 現在の範囲内にある要素数を数える (while 長さの要素が範囲の最大値より小さい (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; つぎの範囲へ移動 (setq top-of-ranges (cdr top-of-ranges)))
さらに、外側のループでは、Emacsはその範囲内にあった定義の個数
(number-within-range
の値)をリストに記録する必要がある。
これにはcons
を使う
(See cons)。
関数cons
で作れるのだが、
最大の範囲に含まれる定義の個数が先頭になり、
最小の範囲に含まれる定義の個数が最後になる。
これは、cons
が新たな要素をリストの先頭に置き、
2つのループが長さのリストの最小のものから処理するので、
defuns-per-range-list
が最大の数を先頭に置いて終わるからである。
しかし、グラフを表示する際には、最小の値を最初に、最大の値を最後に書きたい。
解決策は、defuns-per-range-list
の順番を逆順にすることである。
これには、リストの順番を逆順にする関数nreverse
を使う。
たとえば、
(nreverse '(1 2 3 4))
は、つぎのようになる。
(4 3 2 1)
関数nreverse
は「破壊的」である。
つまり、渡されたリストそのものを変更する。
非破壊的な関数car
やcdr
とは対照的である。
ここでは、もとのdefuns-per-range-list
は必要ないので、
それが破壊されても関係ない
(関数reverse
はリストのコピーを逆順にするので、
もとのリストはそのままである)。
以上をまとめると、defuns-per-range
はつぎのようになる。
(defun defuns-per-range (sorted-lengths top-of-ranges) "SORTED-LENGTHS defuns in each TOP-OF-RANGES range." (let ((top-of-range (car top-of-ranges)) (number-within-range 0) defuns-per-range-list) ;; 外側のループ (while top-of-ranges ;; 内側のループ (while (and ;; 数値判定には数が必要 (car sorted-lengths) (< (car sorted-lengths) top-of-range)) ;; 現在の範囲内にある要素数を数える (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; 内側のループだけを終わる (setq defuns-per-range-list (cons number-within-range defuns-per-range-list)) (setq number-within-range 0) ; カウンタを0にする ;; つぎの範囲へ移動 (setq top-of-ranges (cdr top-of-ranges)) ;; つぎの範囲の最大値を設定する (setq top-of-range (car top-of-ranges))) ;; 外側のループを抜けて、最大の範囲よりも ;; 大きな定義の個数を数える (setq defuns-per-range-list (cons (length sorted-lengths) defuns-per-range-list)) ;; 各範囲の定義の個数のリストを返す ;; 最小から最大の順 (nreverse defuns-per-range-list)))
関数は、1つの微妙な点を除けば、単純である。 内側のループの判定条件はつぎのようである。
(and (car sorted-lengths) (< (car sorted-lengths) top-of-range))
このかわりに、つぎにようにしてみる。
(< (car sorted-lengths) top-of-range)
判定条件の目的は、リストsorted-lengths
の先頭要素が、
範囲の最大値より小さいかどうかを調べることである。
簡略した判定条件は、リストsorted-lengths
がnil
でない限り、
正しく動作する。
しかし、nil
であると式(car sorted-lengths)
はnil
を返す。
関数<
は、数を空リストであるnil
と比較できないので、
Emacsはエラーを通知し、関数の動作を止めてしまう。
リストの最後に達すると、リストsorted-lengths
はつねにnil
になる。
つまり、簡略した判定条件の関数defuns-per-range
を使うと、必ず失敗する。
and
式を使って式(car sorted-lengths)
を追加して、問題を解決する。
式(car sorted-lengths)
は、リストに要素がある限りnil
以外の
値を返すが、リストが空になるとnil
を返す。
and
式は、まず、式(car sorted-lengths)
を評価し、
これがnil
ならば、<
式を評価することなく偽を返す。
しかし、式(car sorted-lengths)
がnil
以外の値を返せば、
and
式は<
式を評価し、その値をand
式の値として返す。
このようにして、エラーを防ぐ。
関数defuns-per-range
を試してみよう。
まず、リストtop-of-ranges
に値の(短かい)リストを束縛する式を評価し、
続いて、リストsorted-lengths
を束縛する式を評価し、
最後に関数defuns-per-range
を評価する。
;; (あとで使うものよりも短いリスト) (setq top-of-ranges '(110 120 130 140 150 160 170 180 190 200)) (setq sorted-lengths '(85 86 110 116 122 129 154 176 179 200 265 300 300)) (defuns-per-range sorted-lengths top-of-ranges)
これはつぎのようなリストを返す。
(2 2 2 0 0 1 0 2 0 0 4)
たしかに、リストsorted-lengths
には、110より小さなものは2つあり、
110と119のあいだのものは2つあり、120と129のあいだのものは2つある。
200を超える値のものは4つある。
われわれの目標は、Emacs Lispのソースコードにあるさまざまな長さの 関数定義の個数を表示したグラフを作ることである。
実際問題として、グラフを作る場合にはgnuplot
などの
プログラムを使うであろう
(gnuplot
はGNU Emacsとうまく組み合わせることができる)。
しかし、ここでは、ゼロからグラフを描くプログラムを作り、
その過程をとおして、すでに学んだことを復習し、より多くを学ぼう。
本章では、単純なグラフを描く関数をまず書いてみる。 この定義はプロトタイプ(prototype)であり、 素早く書いた関数であるが、グラフ作成という未踏領域の探検を可能にしてくれる。 ドラゴンを発見するか単なる伝説であることを知るであろう。 地形を把握できたら、自動的に軸のラベルを描くように関数を拡張する。
Emacsは柔軟で文字端末を含むいかなる種類の端末でも動作するように設計されている ので、「タイプライタ」文字の1つを使ってグラフを作る必要がある。 アスタリスクがよいであろう。 グラフ表示関数を拡張すれば、 ユーザーのオプションで文字を選択できるようにもできる。
この関数をgraph-body-print
としよう。
唯一の引数としてnumbers-list
を取る。
ここでは、グラフにはラベルを付けずに、本体のみを表示することにする。
関数graph-body-print
は、numbers-list
の各要素ごとに
アスタリスクから成る縦のコラムを挿入する。
各コラムの高さはnumbers-list
の対応する要素の値で決まる。
コラムの挿入は繰り返し動作なので、
この関数はwhile
ループや再帰を使って書ける。
第1段階として、アスタリスクのコラムをどのように表示するかを考えよう。 通常、Emacsでは、文字を画面の水平方向に、行単位で表示する。 2つの方法で対処することができる。 独自のコラム挿入関数を書くか、Emacsの既存のものを探すかである。
Emacsに既存のものがあるかどうかを調べるには、コマンドM-x aproposを使う。 このコマンドは、コマンドC-h a(command-apropos)に似ているが、 後者はコマンドとなる関数のみを探す点が異なる。 コマンドM-x aproposは、対話的でない関数も含めて、 正規表現に一致するすべてのシンボルを表示する。
探したいコマンドは、コラムを表示したり挿入するコマンドである。
関数名には、「print」や「insert」や「column」の単語が含まれるであろう。
そこで、M-x apropos RET print\|insert\|column RETとタイプして
結果を見てみよう。
筆者のシステムでは、しばらくしてから、79個の関数や変数を表示した。
この一覧を調べた結果、それらしい唯一の関数はinsert-rectangle
であった。
たしかにこれがほしい関数であり、その説明文はつぎのとおりである。
insert-rectangle: Insert text of RECTANGLE with upper left corner at point. RECTANGLE's first line is inserted at point, its second line is inserted at a point vertically under point, etc. RECTANGLE should be a list of strings.
予想どおりに動作するかどうか調べてみよう。
insert-rectangle
式の直後にカーソルを置いて
C-u C-x C-eとタイプした結果をつぎに示す。
この関数は、ポイントの直後から下向きに
"first"
と"second"
と"third"
を挿入した。
また、関数はnil
を返した。
(insert-rectangle '("first" "second" "third"))first second third nil
もちろん、このinsert-rectangle
式自身のテキストをバッファに
挿入したいのではないが、われわれのプログラムからこの関数を呼び出す。
関数insert-rectangle
が文字列のコラムを挿入する場所に
ポイントを正しく移動しておく必要もある。
Infoで読んでいる場合には、バッファ*scratch*
などの
別のバッファに切り替え、バッファの適当な場所へポイントを移動し、
M-<ESC>とタイプして、ミニバッファの問い合わせに
insert-rectangle
式をタイプして<RET>をタイプすれば、
この動作を調べることができる。
これにより、Emacsはミニバッファの式を評価するが、
ポイントの値としては、バッファ*scratch*
のポイントの位置を使う
(M-<ESC>は、eval-expression
のキーバインドである)。
ポイントは最後に挿入した行の直後に移動していることがわかる。 つまり、この関数は、副作用としてポイントを移動する。 この位置でコマンドを繰り返すと、直前の挿入位置の右に下向きに挿入される。 これでは困る! 棒グラフを描くときには、コラムが互いに並んでいる必要がある。
コラムを挿入するwhile
ループの各繰り返しでは、ポイントを移動して
コラムの最後ではなくコラムの先頭に置く必要があることがわかる。
さらに、グラフを描くとき、すべてのコラムが同じ高さではない。
つまり、各コラムの先頭は、直前のものとは異なった高さにある。
単純にいつも同じ行にポイントを位置決めすることはできず、
正しい位置に移動する必要がある。
たぶんこうできるだろう...。
アスタリスクで表した棒グラフを描きたいのである。
各コラムのアスタリスクの個数は、numbers-list
の要素で決まる。
insert-rectangle
の各呼び出しでは、
正しい長さのアスタリスクのリストを作る必要がある。
必要な個数のアスタリスクだけでこのリストが作られている場合、
グラフを正しく表示するには、基準行から正しい行数だけポイントを上に
位置決めする必要がある。
これは、難しい。
かわりに、つねに同じ長さのリストをinsert-rectangle
に渡すことができれば、
新たにコラムを追加するたびに右へ移動する必要はあるが、
ポイントは同じ行に置ける。
このようにした場合、insert-rectangle
に渡すリストの一部は
アスタリスクではなく空白にする必要がある。
たとえば、グラフの最大の高さが5で、コラムの高さが3だとすると、
insert-rectangle
にはつぎのような引数が必要になる。
(" " " " "*" "*" "*")
コラムの高さがわかれば、このようなことは難しくない。
コラムの高さを指定する方法は2つある。
適当な高さをあらかじめ指定しておけば、その高さまでのグラフは正しく描ける。
あるいは、数のリストを調べて、リストの最大値をグラフの最大の高さとする。
後者の処理が難しければ、前者の処理がもっとも簡単である。
Emacsには引数の最大値を調べる組み込み関数がある。
その関数を使おう。
関数はmax
であり、数である全引数の中の最大値を返す。
たとえば、
(max 3 4 6 5 7 3)
は7を返す
(対応する関数min
は、全引数の中の最小値を返す)。
しかし、単純にnumbers-list
に対してmax
を呼べない。
関数max
は、引数として数のリストではなく数を要求する。
したがって、つぎの式、
(max '(3 4 6 5 7 3))
は、つぎのようなエラーメッセージを出す。
Wrong type of argument: integer-or-marker-p, (3 4 6 5 7 3)
引数のリストを関数に渡す関数が必要である。 この関数は、第1引数(関数)を残りの引数に「適用(applies)」する。 なお、最後の引数はリストでもよい。
たとえば、
(apply 'max 3 4 7 3 '(4 8 5))
は、8を返す。
(本書のような書籍なしで、この関数をどのように学ぶのか筆者にはわからない。
関数名の一部を予想してapropos
を使えば、
search-forward
やinsert-rectangle
などのこれ以外の関数を
探すのは可能である。
第1引数を残りに「適用(apply)」するという隠喩は明らかであるにも関わらず、
初心者がapropos
や他の補佐機能を使うときに、この用語を思い付くとは
思えない。
もちろん、筆者がまちがっている可能性もあるが、
いずれにしても、関数は、それを最初に発明した人が命名する。)
apply
の2番目以降の引数は省略できるので、
リストの要素を渡して関数を呼び出すためにapply
を使える。
つぎのようにしても「8」を返す。
(apply 'max '(4 8 5))
apply
をこの方法で使うことにする。
関数recursive-lengths-list-many-files
は、
max
を適用する数のリストを返す
(ソートした数のリストにmax
を適用することもできる。
リストがソートされているかいないかは関係ない)。
したがって、グラフの最大の高さを調べる操作は、つぎようになる。
(setq max-graph-height (apply 'max numbers-list))
グラフのコラムを表す文字列のリストの作り方に戻ろう。
グラフの最大の高さとコラムに現れるべきアスタリスクの個数を与えられて、
関数はコマンドinsert-rectangle
で挿入すべき文字列のリストを返す。
各コラムは、アスタリスクか空白文字である。
関数はコラムの高さの値とコラム内のアスタリスクの個数を与えられるので、
空白の個数は、コラムの高さからアスタリスクの個数を引けば計算できる。
空白の個数とアスタリスクの個数を与えられ、
2つのwhile
ループでリストを作る。
;;; 第1版 (defun column-of-graph (max-graph-height actual-height) "Return list of strings that is one column of a graph." (let ((insert-list nil) (number-of-top-blanks (- max-graph-height actual-height))) ;; アスタリスクを埋める (while (> actual-height 0) (setq insert-list (cons "*" insert-list)) (setq actual-height (1- actual-height))) ;; 空白を埋める (while (> number-of-top-blanks 0) (setq insert-list (cons " " insert-list)) (setq number-of-top-blanks (1- number-of-top-blanks))) ;; リスト全体を返す insert-list))
この関数をインストールして、つぎの式を評価すれば、 目的のリストが返されることがわかる。
(column-of-graph 5 3)
は、つぎのリストを返す。
(" " " " "*" "*" "*")
このcolumn-of-graph
には1つの大きな欠陥がある。
コラムの空白や印として使うシンボルを、空白文字とアスタリスクに
「書き込んである(hard-coded)」ことである。
プロトタイプとしてはよいが、別のシンボルを使いたい人もいるだろう。
たとえば、グラフ関数をテストするときには、空白のかわりにピリオドを使って、
関数insert-rectangle
を呼ぶたびにポイントが正しくなっていることを
確かめたい。
あるいは、アスタリスクのかわりに+
や別の記号を使いたいであろう。
コラム幅を1文字より大きくしたい場合もあろう。
プログラムはより柔軟であるべきである。
これには、空白文字とアスタリスクのかわりに、
2つの変数、graph-blank
とgraph-symbol
を使い、
これらの変数に別々に値を定義する。
また、説明文も十分ではない。 これらを考慮すると、つぎの第2版になる。
(defvar graph-symbol "*" "String used as symbol in graph, usually an asterisk.") (defvar graph-blank " " "String used as blank in graph, usually a blank space. graph-blank must be the same number of columns wide as graph-symbol.")
(defvar
の説明は、
defvarを参照。)
;;; 第2版 (defun column-of-graph (max-graph-height actual-height) "Return list of MAX-GRAPH-HEIGHT strings; ACTUAL-HEIGHT are graph-symbols. The graph-symbols are contiguous entries at the end of the list. The list will be inserted as one column of a graph. The strings are either graph-blank or graph-symbol." (let ((insert-list nil) (number-of-top-blanks (- max-graph-height actual-height))) ;;graph-symbols
を埋め込む (while (> actual-height 0) (setq insert-list (cons graph-symbol insert-list)) (setq actual-height (1- actual-height))) ;;graph-blanks
を埋め込む (while (> number-of-top-blanks 0) (setq insert-list (cons graph-blank insert-list)) (setq number-of-top-blanks (1- number-of-top-blanks))) ;; リスト全体を返す insert-list))
必要ならば、column-of-graph
をもう一度書き直して、
棒グラフにするか折線グラフにするを決めるオプションを
与えられるようにもできる。
これは難しくはない。
折線グラフは、各バーの先頭より下が空白の棒グラフであると考えられる。
折線グラフのコラムを作るには、まず、値より1小さい空白文字のリストを作り、
cons
を使って印のシンボルをリストに繋げ、
cons
を使ってリストの先頭に空白文字を埋め込む。
このような関数の書き方は簡単であるが、
われわれには必要ないのでやらないことにする。
しかし、そのようにするならばcolumn-of-graph
を書き直す。
より重要なことは、別の部分には何の変更も必要ないことに注意してほしい。
強調するが、やろうと思えば簡単にできる。
では、グラフを描く最初の実際の関数を書いてみよう。
これはグラフの本体を描くが、垂直軸や水平軸のラベルを描かないので、
この関数をgraph-body-print
と呼ぶことにする。
graph-body-print
前節までの準備があるので、関数graph-body-print
は簡単である。
この関数は、数のリストの各要素が各コラムのアスタリスクの個数を指定するものと
して、アスタリスクや空白文字から成るコラムを描く。
これは繰り返し動作なので、減少方式のwhile
ループや再帰的関数で書ける。
本節では、while
ループを使った定義を書こう。
関数column-of-graph
は、引数としてグラフの高さが必要であるので、
これをローカル変数とする。
この関数のwhile
ループの雛型はつぎのようになる。
(defun graph-body-print (numbers-list) "説明文..." (let ((height ... ...)) (while numbers-list コラムを挿入し、ポイントを再位置決めする (setq numbers-list (cdr numbers-list)))))
この雛型の項目を埋めていこう。
グラフの高さを求めるには、式(apply 'max numbers-list)
を使う。
while
ループは、numbers-list
の要素を一度に1つずつ処理する。
リストを短くするには式(setq numbers-list (cdr numbers-list))
を使う。
リストのCARはcolumn-of-graph
の引数である。
while
の各繰り返しでは、関数insert-rectangle
で、
column-of-graph
が返したリストを挿入する。
関数insert-rectangle
は、挿入位置の右下にポイントを移動するので、
挿入するときのポイントの値を保存し、挿入後にポイントを戻し、
つぎにinsert-rectangle
を呼び出すために水平方向に移動する。
挿入するコラムが1文字幅ならば、
再位置決めコマンドは単に(forward-char 1)
でよい。
しかし、コラムの幅が1文字を越えるかもしれない。
つまり、再位置決めコマンドは(forward-char symbol-width)
とすべきである。
symbol-width
は、graph-blank
の長さであり、
式(length graph-blank)
で調べる。
変数symbol-width
をコラム幅にバインドする最適の場所は、
let
式の変数リストである。
以上を考慮すると関数定義はつぎのようになる。
(defun graph-body-print (numbers-list) "Print a bar graph of the NUMBERS-LIST. The numbers-list consists of the Y-axis values." (let ((height (apply 'max numbers-list)) (symbol-width (length graph-blank)) from-position) (while numbers-list (setq from-position (point)) (insert-rectangle (column-of-graph height (car numbers-list))) (goto-char from-position) (forward-char symbol-width) ;; コラムごとにグラフを描く (sit-for 0) (setq numbers-list (cdr numbers-list))) ;; 水平軸のラベル用にポイントを置く (forward-line height) (insert "\n") ))
この関数定義で予期しなかった式は、
while
ループの中の式(sit-for 0)
である。
この式は、グラフ表示操作を見ているとおもしろくする。
この式は、Emacsに「じっと(sit)している」ように、つまり、
0時間のあいだ何もしないで、画面を再描画させる指示である。
ここに書くことで、Emacsはコラムごとに画面を再描画する。
これがないと、関数が終了するまでEmacsは画面を再描画しない。
数の短いリストでgraph-body-print
を試そう。
graph-symbol
、graph-blank
、column-of-graph
、
graph-body-print
をインストールする。
(graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3))
*scratch*
に切り替え、
グラフを描き始める位置にカーソルを置く。
eval-expression
)とタイプする。
yank
)でgraph-body-print
式をヤンクする。
graph-body-print
式を評価する。
Emacsはつぎのようなグラフを描く。
* * ** * **** *** **** ********* * ************ *************
recursive-graph-body-print
関数graph-body-print
は、再帰的に書くこともできる。
この場合には、2つの部分に分ける。
グラフの最大の高さなどの一度だけ探す必要がある変数の値を決めるための
let
式を使った外側の「呼び出し側関数(wrapper)」と、
グラフを書くために再帰的に呼び出される内側の関数である。
「呼び出し側関数」は簡単である。
(defun recursive-graph-body-print (numbers-list) "Print a bar graph of the NUMBERS-LIST. The numbers-list consists of the Y-axis values." (let ((height (apply 'max numbers-list)) (symbol-width (length graph-blank)) from-position) (recursive-graph-body-print-internal numbers-list height symbol-width)))
再帰関数は少々込み入っている。
これには4つの部分がある。
「再帰条件」、コラムを書くコード、再帰呼び出し、「次段式」である。
「再帰条件」はif
式であり、numbers-list
に要素が
残っているかどうかを調べる。
そうならば、書くコードを使って1つのコラムを書き、自身を再度呼び出す。
関数が自身を再度呼び出す場合には、「次段式」で短くした
numbers-list
の値を使う。
(defun recursive-graph-body-print-internal (numbers-list height symbol-width) "Print a bar graph. Used within recursive-graph-body-print function." (if numbers-list (progn (setq from-position (point)) (insert-rectangle (column-of-graph height (car numbers-list))) (goto-char from-position) (forward-char symbol-width) (sit-for 0) ; コラムごとにグラフを描く (recursive-graph-body-print-internal (cdr numbers-list) height symbol-width))))
インストールしてから試してみよう。 たとえばつぎのようにする。
(recursive-graph-body-print '(3 2 5 6 7 5 3 4 6 4 3 2 1))
recursive-graph-body-print
はつぎのような出力をする。
* ** * **** * **** *** * ********* ************ *************
これら2つの関数、graph-body-print
やrecursive-graph-body-print
の
いずれも、グラフの本体を作る。
グラフを読み取るためには、グラフの軸が必要である。 一度だけならば、Emacsのピクチャーモードを使って、 手で軸を描くのも合理的であろう。 しかし、グラフ描画関数は何回も使われる。
このため、基本の関数print-graph-body
を拡張して、
水平軸と垂直軸のラベルを自動的に描くようにした。
ラベル表示関数には新しいことがらはないので、のちほど付録で説明する。
See Full Graph。
折線グラフを描くグラフ表示関数を書いてみよ。
.emacs
「Emacsを好きになるにはEmacsが好きである必要はない」。 この矛盾するような言葉は、GNU Emacsの秘密である。 「箱から取り出した」ままのEmacsは、汎用のツールである。 Emacsを使うほとんどの人は、満足のいくようにカスタマイズする。
GNU Emacsは、ほとんどEmacs Lispで書かれている。 つまり、Emacs Lispの式を書けば、Emacsを修正したり拡張できるのである。
.emacs file
.
Emacsのデフォルトの設定に満足する人もいる。 Emacsは、Cのファイルを編集するとCモードで、 Fortranのファイルを編集するとFortanモードで、 普通のファイルを編集すると基本(Fundamental)モードで始まる。 誰がEmacsを使おうとしているのかわからない場合には、 これらはすべて意味があることである。 普通のファイルで何をしたいか予測できる人がいるだろうか? Cのコードを編集するときにはCモードが正しいデフォルトであるように、 基本モードはそのようなファイルに対する正しいデフォルトである。 しかし、読者自身がEmacsを使う場合には、 Emacsをカスタマイズすることに意味がある。
たとえば、筆者は特定のファイル以外では基本モードを好み、Textモードを好む。 そのために、Emacsをカスタマイズして、筆者に適するようにする。
~/.emacs
ファイルを書くことで、
Emacsをカスタマイズしたり拡張できる。
これは、個人用の初期化ファイルであり、
その内容はEmacs Lispで書かれており、Emacsに何をすべきかを指示する。
本章では、簡単な~/.emacs
ファイルについて説明する。
より詳しくは、Init File
やInit Fileを参照してほしい。
個人用の初期化ファイルに加えて、
Emacsはサイト全体のさまざまな初期化ファイルを自動的にロードする。
これらは個人用の~/.emacs
ファイルと同じ形式であるが、
誰もがロードするものである。
サイト全体の2つの初期化ファイル、site-load.el
とsite-init.el
は、
もっとも一般的に使われるEmacsの「ダンプ版」を
作成するときにEmacsにロードされる
(ダンプ版Emacsは、素早くロードできる。
しかし、いったんファイルをロードしてダンプすると、
変更をロードし直したり再度ダンプし直さない限り、その変更は反映されない。
ファイルINSTALL
や
See Building Emacs)。
Emacsを起動するたびに、サイト全体の3つの初期化ファイルが自動的にロードされる。
これらは、個人の.emacs
ファイルをロードするまえにロードされる
site-start.el
と、個人の.emacs
ファイルをロードしたあとに
ロードされるdefault.el
と端末タイプファイルである。
個人の.emacs
ファイル内の設定や定義は、
ファイルsite-start.el
内の重複する設定や定義を上書きする。
しかし、default.el
や端末タイプファイルでの設定や定義は、
個人の.emacs
ファイルでの設定や定義を上書きする
(端末タイプファイルの干渉を防ぐには、
term-file-prefix
にnil
を設定する。
See Simple Extension)。
ディストリビューションに含まれるファイルINSTALL
には、
ファイルsite-init.el
とsite-load.el
に関する説明がある。
ファイルloadup.el
、startup.el
、loaddefs.el
は、
ファイルのロードを制御する。
これらのファイルは、Emacsディストリビューションのディレクトリlisp
に
あり、見ておく価値がある。
ファイルloaddefs.el
には、個人の.emacs
ファイルや
サイト全体の初期化ファイルに何を書くべきかについて多くの有用な助言がある。
筆者のEmacs第19.23版には、コマンドedit-options
で設定可能な
オプションが392個ある。
これらの「オプション」は、これまでに説明したきた変数と何ら変わらず、
defvar
を用いて定義されている。
Emacsは、変数の説明文字列の最初の文字を調べて、
その変数が設定用のものかどうかを判断する。
最初の文字がアスタリスク*
ならば、
変数はユーザーが設定可能なオプションである
(See defvar)。
コマンドedit-options
は、Emacs Lispのライブラリ作成者が
ユーザーが設定してよいと判断したEmacs内のすべての変数を一覧表示する。
これらの変数を再設定する使いやすいインターフェイスを提供する。
一方、edit-options
で設定したオプションは、
現在の作業中でのみ有効である。
新しい値は、作業を越えては保存されない。
Emacsを起動するたびに、ソースコードのもとのdefvar
の値を読むことになる。
設定変更を次回の作業でも有効にするには、
.emacs
ファイルや起動時にロードする他のファイルで、
setq
式を使う必要がある。
筆者の場合、コマンドedit-options
の主な利用目的は、
.emacs
ファイルに設定する変数を探すことである。
一覧表示を読むことを強く勧める。
より詳しくは、 See Edit Options。
.emacs
入門Emacsを起動すると、コマンド行で-q
を指定して
.emacs
ファイルを読まないように指示しないかぎり、
Emacsは個人用の.emacs
ファイルをロードする
(コマンドemacs -q
により、箱から取り出したままのEmacsを使える)。
.emacs
ファイルには、Lispの式が収めてある。
しばしば、値を設定する式だけの場合もある。
関数定義がある場合もある。
初期化ファイルの概略については、 See Init File。
本章では、長期に渡って使ってきた完全な.emacs
ファイルを説明する。
つまり、筆者の.emacs
ファイルである。
最初の部分は注釈であり、自分用のメモである。 今ではわかっているが、始めた頃はそうではなかった。
;;;; Bob's .emacs file ; Robert J. Chassell ; 26 September 1985
日付を見てほしい! だいぶまえにこのファイルを使い始めた。 それ以来、追加し続けている。
; Each section in this file is introduced by a ; line beginning with four semicolons; and each ; entry is introduced by a line beginning with ; three semicolons.
この部分は、Emacs Lispの注釈の習慣を述べたものである。 セミコロン以降はすべて注釈である。 セミコロンの数で、節や小文を表している (注釈について詳しくは、 See Comments)。
;;;; The Help Key ; Control-h is the help key; ; after typing control-h, type a letter to ; indicate the subject about which you want help. ; For an explanation of the help facility, ; type control-h three times in a row.
ヘルプを起動するにはC-hを3回タイプすることの メモ書きである。
; To find out about any mode, type control-h m ; while in that mode. For example, to find out ; about mail mode, enter mail mode and then type ; control-h m.
筆者はこれを「モードヘルプ」と読んでいるが、これはとても助けになる。 普通、知っておくべきことをすべて教えてくれる。
もちろん、読者の.emacs
ファイルに、これらの注釈を含める必要はない。
筆者のファイルに入れておいたのは、モードヘルプや注釈の習慣を忘れやすかった
からである。
ここに書いておけば、見れば思い出せる。
TextモードとAuto Fillモードを有効にする部分である。
;;; Text mode and Auto Fill mode ; The next two lines put Emacs into Text mode ; and Auto Fill mode, and are for writers who ; want to start writing prose rather than code. (setq default-major-mode 'text-mode) (add-hook 'text-mode-hook 'turn-on-auto-fill)
これは、物忘れしやすい人間へのメモ以外のことを初めて行う
.emacs
ファイルの部分である。
最初の括弧の中の2行は、ファイルを探したときに、 Cモードなどの他のモードでない場合には、Textモードを有効にする指示である。
Emacsがファイルを読み込むとき、ファイル名の拡張子を調べる
(拡張子は.
のあとに続く部分である)。
ファイルが.c
や.h
の拡張子で終わっていると、
EmacsはCモードを有効にする。
また、Emacsはファイルの最初の空行でない行を調べ、
その行が-*- C -*-
となっていると、EmacsはCモードを有効にする。
Emacsは、自動的に使用する拡張子とモード指定のリストを持っている。
さらに、Emacsは、バッファごとの「ローカル変数リスト」を調べるために
ファイルの最後のページも調べる。
.emacs
ファイルに戻ろう。
この行はどのように働くのであろうか?
(setq default-major-mode 'text-mode)
この行は短いが、Emacs Lispの完全な式である。
setq
についてはすでによく知っている。
変数default-major-mode
に、値text-mode
を設定する。
text-mode
の直前の引用符は、
Emacsに変数text-mode
を直接扱うことを指示する。
setq
の残りの動作については、See set & setq。
重要な点は、.emacs
ファイルで値を設定する方法と
Emacsのそれ以外の場面で値を設定する方法とは何ら変わらないことである。
2行目はつぎのとおりであった。
(add-hook 'text-mode-hook 'turn-on-auto-fill)
この行では、コマンドadd-hook
で、
変数text-mode-hook
にturn-on-auto-fill
を追加する。
turn-on-auto-fill
は、読者の予想どおり、プログラムの名前であり、
Auto Fillモードを有効にする。
Textモードを有効にするたびに、 EmacsはTextモードにフックしたコマンドを実行する。 つまり、Textモードを有効にするたびに、Auto Fillモードも有効にする。
まとめると、1行目は、ファイル名の拡張子や最初の空行以外の行や ローカル変数で指定されない限り、ファイルを編集するときは、 Textモードを有効にする指示である。
Textモードは、他のことも含めて、普通の文書作成に適した
シンタックステーブルを設定する。
Textモードでは、Emacsは引用符を単語の一部として扱う。
しかし、ピリオドや空白は単語の一部ではない。
したがって、M-fでit's
を飛び越せる。
一方、Cモードでは、M-fはit's
のt
の直後で止まる。
2行目は、Textモードを有効にしたらAuto Fillモードも有効にする指示である。 Auto Fillモードでは、Emacsは自動的に長すぎる行を分割し、つぎの行へ送る。 Emacsは、単語の途中ではなく、単語と単語のあいだで行を分ける。
Auto Fillモードを無効にすると、タイプしたとおりの行分けになる。
truncate-lines
に設定した値に依存して、
タイプした単語が画面の右端から消えてしまうか、
見にくいが画面上に継続行として表示される。
メモ書きも含めて、メールの別名を有効にするsetq
である。
;;; Mail mode ; To enter mail mode, type `C-x m' ; To enter RMAIL (for reading mail), ; type `M-x rmail' (setq mail-aliases t)
コマンドsetq
で、変数mail-aliases
の値をt
にする。
t
は真を意味するので、この行の意味は、
「メールの別名を使うようにする」である。
メールの別名は、長い電子メールアドレスや複数の電子メールアドレスに
短縮名を付ける便法である。
「別名(aliases)」を保持するファイルは、~/.mailrc
である。
別名はつぎのように書く。
alias geo george@foobar.wiz.edu
Georgeにメッセージを書くときには、アドレスgeo
を使う。
メール転送プログラムは、自動的にgeo
を完全なアドレスに展開する。
デフォルトでは、リージョンを整形するときに、
Emacsは複数の空白のかわりにタブを挿入する
(たとえば、コマンドindent-region
で一度に複数の行を字下げする場合)。
端末や普通の印刷ではタブは適しているのだが、
TeXはタブを無視するのでTeXやTexinfoでの字下げには適さない。
つぎの行は、Indent Tabsモードを無効にする。
;;; Prevent Extraneous Tabs (setq-default indent-tabs-mode nil)
これまでのようにコマンドsetq
ではなく、
setq-default
を使っていることに注意してほしい。
コマンドsetq-default
は、
その変数にバッファ独自の値が設定されていないバッファでのみ値を設定する。
つぎは、個人用のキーバインドである。
;;; Compare windows (global-set-key "\C-cw" 'compare-windows)
compare-windows
は、カレントウィンドウとつぎのウィンドウの
テキストを比較するちょっとしたコマンドである。
両者のウィンドウのポイント位置から始めて、一致する限り
両者のウィンドウのテキストを比較する。
筆者はこのコマンドをよく使う。
これは、すべてのモードに対して大局的にキーを設定する方法でもある。
コマンドはglobal-set-key
である。
これに続けてキーバインドを書く。
.emacs
ファイルでは、ここに記したようにキーバインドを書く。
\C-c
は「control-c」のことで、
コントロールキーとcキーを同時に押し下げることを意味する。
w
はwキーを押し下げることを意味する。
キーバインドは二重引用符で囲む。
説明文章などでは、これをC-c wと書く
(<CTL>キーのかわりにM-cのように<META>をバインドする場合には、
\M-c
と書く。
詳しくは、See Init Rebinding)。
このキーで起動されるコマンドはcompare-windows
である。
コマンドの直前に引用符があることに注意してほしい。
さもないと、Emacsは、値を得るためにシンボルを評価しようとする。
これらの3つ、二重引用符、C
の直前のバックスラッシュ、引用符は、
筆者が忘れがちなキーバインドの必須項目である。
幸い、自分の.emacs
ファイルを見ればよいことを覚えているので、
そこに書いているとおりにできる。
キーバインドに関していえば、C-c wは、 前置キーC-cと単一文字wの組み合わせである。 C-cに単一文字を続けるキーの組み合わせは、各個人用に予約されている。 Emacsの拡張を書く場合には、これらのキーの組み合わせを使わないようにする。 かわりに、C-c C-wのようなキーの組み合わせを使う。 さもないと、各個人用に使えるキーの組み合わせがなくなってしまう。
注釈を付けた別のキーバインドもある。
;;; Keybinding for `occur' ; I use occur a lot, so let's bind it to a key: (global-set-key "\C-co" 'occur)
コマンドoccur
は、
正規表現に一致するカレントバッファの行をすべて表示する。
一致した行はバッファ*Occur*
に表示される。
このバッファは、一致した行へ飛ぶためのメニューの役割を果たす。
キーのバインドを解除する方法もある。
;;; Unbind `C-x f' (global-unset-key "\C-xf")
このバインドを解除する理由は、筆者の場合C-x C-fとタイプするつもりで C-x fとタイプすることが多いからである。 意図した操作であるファイルを探すかわりに、 たいていの場合、望みの値ではない追い込み幅を誤って設定してしまう。 デフォルトの幅を再設定することはほとんどないので、キーのバインドを解除した。
つぎは、既存のキーバインドをリバインドする。
;;; Rebind `C-x C-b' for `buffer-menu' (global-set-key "\C-x\C-b" 'buffer-menu)
デフォルトでは、C-x C-bはコマンドlist-buffers
を実行する。
このコマンドは、別のコマンドにバッファ一覧を表示する。
筆者はそのウィンドウでほとんどの場合、何かをしたくなるので、
バッファ一覧を表示するだけでなく、そのウィンドウにポイントも移動する
コマンドbuffer-menu
を好むのである。
GNU Emacsの利用者の多くの人が、Emacsの拡張を書いている。 時が経つにつれて、これらの拡張はしばしば新しいリリースに取り込まれる。 たとえば、カレンダーや日記のパッケージは、いまでは、 Emacs第19版の標準ディストリビューションの一部である。 しかし、Emacs第18版の標準ディストリビューションの一部ではなかった。
(筆者がEmacsの重要な部分であると考えるCalcは、標準ディストリビューション の一部になるであろうが、巨大なので別配布になっている。)
コマンドload
を使えば、ファイル全体を評価してファイル内の
関数や変数をEmacsにインストールできる。
たとえばつぎのようにする。
(load "~/emacs/kfill")
これは、読者のホームディレクトリ下のディレクトリemacs
から
ファイルkfill.el
(バイトコンパイルしたファイルkfill.elc
が
あればそれ)を評価する、つまり、ロードする。
(kfill.el
は、Kyle E. Jonesのfilladapt.el
パッケージを
Bob Weinerが適応したもので、仰々しい単語の追い込みはせずに、
ニューズやメールのメッセージ、Lisp、C++、PostScript、シェルの注釈などの
テキストを字下げを保存して段落に追い込む。
筆者はいつもこれを使っており、
標準ディストリビューションに組み込まれることを望んでいる。)
筆者のように多数の拡張をロードする場合、
上のように拡張ファイルの正確な場所を指定するかわりに、
そのディレクトリをEmacsのload-path
に追加指定できる。
すると、Emacsがファイルをロードするとき、デフォルトのディレクトリに加えて、
そのディレクトリも探す
(デフォルトのディレクトリは、Emacsを構築するときにpaths.h
で指定する)。
つぎのコマンドは、既存のロードパスに
読者のディレクトリ~/emacs
を追加する。
;;; Emacs Load Path (setq load-path (cons "~/emacs" load-path))
load-library
は、関数load
の対話的インターフェイスである。
関数全体はつぎのようである。
(defun load-library (library) "Load the library named LIBRARY. This is an interface to the function `load'." (interactive "sLoad library: ") (load library))
関数名load-library
は、
ファイルの慣習的な同義語である「library(ライブラリ)」からきている。
コマンドload-library
のソースはライブラリfiles.el
にある。
少々異なるが同じような処理を行う対話的コマンドはload-file
である。
load-library
とこのコマンドとの違いに
関しては、See Lisp Libraries。
関数を収めたファイルをロードしたり関数定義を評価したりして関数を インストールするかわりに、関数を利用できるようにはするが、 最初に呼び出すまで実際にはインストールしないようにできる。 これをオートロード(autoloading)という。
オートロードする関数を評価すると、Emacsは、関数を収めたファイルを 自動的に評価してから関数を呼び出す。
オートロードする関数を使うと、ライブラリをただちにロードする必要がないので、 Emacsの起動が素早くなる。 しかし、そのような関数を最初に使うときには、 それらを収めたファイルを評価するので、多少待つ必要がある。
あまり使わないような関数はしばしばオートロードの対象になる。
ライブラリloaddefs.el
には、bookmark-set
からwordstar-mode
までの何百ものオートロードする関数が定義されている。
もちろん、「あまり」使わないような関数を頻繁に使う場合もあろう。
その場合には、.emacs
ファイルでload
式を使って関数のファイルを
ロードするようにする。
Emacs第19.23版用の筆者の.emacs
ファイルでは、
オートロードされる関数の内、17個のライブラリをロードしている
(実際には、ダンプ版Emacsを作るときにこれらのファイルを
含めておくべきだが、忘れていた。
ダンプについて詳しくは、ファイルINSTALL
や
See Building Emacs)。
読者が、.emacs
ファイルにオートロードの式を含めたい場合もあろう。
autoload
は組み込み関数であり、最大で5つの引数を取るが、
最後の3つは省略できる。
第1引数は、オートロードする関数の名前である。
第2引数は、ロードすべきファイルの名前である。
第3引数は、関数の説明文であり、
第4引数は、関数を対話的に呼ぶかどうかを指定する。
第5引数は、オブジェクトの型であり、
autoload
は関数(デフォルト)に加えてキーマップやマクロも扱える。
典型的な例はつぎのとおりである。
(autoload 'html-helper-mode "html-helper-mode" "Edit HTML documents" t)
この式は、関数html-helper-mode
をオートロードする。
ファイルhtml-helper-mode.el
(バイトコンパイルしたファイルhtml-helper-mode.elc
があれば、それ)
を読み込む。
ファイルは、load-path
に指定したディレクトリにある必要がある。
説明文の内容は、このモードはハイパーテキストマークアップ言語で
書かれた文章の編集を助けるである。
M-x html-helper-modeとタイプすると対話的にこのモードになる
(関数がロードされていないと説明文も利用できないので、
関数の説明文をオートロードの式に複製する必要がある)。
詳しくは、See Autoload。
line-to-top-of-window
ウィンドウの先頭行にポイントを移動するEmacsへの簡単な拡張を説明する。 筆者は、テキストを読みやすくするためこれをよく使っている。
以下のコードを別のファイルに収めて、そのファイルを.emacs
ファイルで
ロードしてもよいし、コードを.emacs
ファイルに収めてもよい。
定義はつぎのとおりである。
;;; Line to top of window; ;;; replace three keystroke sequence C-u 0 C-l (defun line-to-top-of-window () "Move the line point is on to top of window." (interactive) (recenter 0))
つぎは、キーバインドである。
Emacs第18版の.emacs
ファイルのほとんどは、
第19版でも使えるが、多少異なるものもある
(もちろん、これらはEmacs第19版の新機能でもある)。
Emacs第19版では、[f6]
のようにファンクションキーを書ける。
第18版では、ファンクションキーを押したときに送られるキー列を
指定する必要がある。
たとえば、Zenith 29キーボードでは、6番目のファンクションキーを押すと
<ESC> <P>が送られる。
Ann Arbor Ambassadorキーボードでは、<ESC> <O> <F>が送られる。
これらのキー列は、それぞれ、\eP
、\eOF
と書く。
筆者の第18版用の.emacs
ファイルでは、
line-to-top-of-window
をキーボードの種類に依存したキーに
バインドしている。
(defun z29-key-bindings () "Function keybindings for Z29 terminal." ;; ... (global-set-key "\eP" 'line-to-top-of-window)) (defun aaa-key-bindings () "Function keybindings for Ann Arbor Ambassador" ;; ... (global-set-key "\eOF" 'line-to-top-of-window))
(ファンクションキーがどんなキー列を送るかを調べるには、
ファンクションキーを押してからC-h l(view-lossage
)とタイプする。
このコマンドは、最新の100個のキー列を表示する。)
キーバインドを指定したら、端末の種類に応じたキーバインドの式を評価する。
しかし、そのまえに、重複していると.emacs
ファイルの設定を上書きしてしまう。
あらかじめ定められたデフォルトの端末固有のキーバインドの設定を無効にする。
;;; Turn Off Predefined Terminal Keybindings ; The following turns off the predefined ; terminal-specific keybindings such as the ; vt100 keybindings in lisp/term/vt100.el. ; If there are no predefined terminal ; keybindings, or if you like them, ; comment this out. (setq term-file-prefix nil)
端末の種類に応じた選択はつぎのように行う。
(let ((term (getenv "TERM"))) (cond ((equal term "z29") (z29-key-bindings)) ((equal term "aaa") (aaa-key-bindings)) (t (message "No binding for terminal type %s." term))))
Emacs第19版では、(マウスボタンイベントや非ASCII文字と同様に)
ファンクションキーは、引用符を付けずに鈎括弧に囲んで書く。
line-to-top-of-window
をファンクションキー<F6>にバインドするには、
つぎのようにする。
(global-set-key [f6] 'line-to-top-of-window)
こちらのほうが簡単である!
より詳しくは、Init Rebindingを参照。
Emacs第18版とEmacs第19版の両方を使っている場合には、 つぎの場合分けでどちらの式を評価するかを選択できる。
(if (string= (int-to-string 18) (substring (emacs-version) 10 12)) ;; evaluate version 18 code (progn ... ) ;; else evaluate version 19 code ...
Emacsでは、どのキーがどのコマンドを呼ぶかを記録するために キーマップ(keymaps)を使っている。 Cモード、Textモードなどの特定のモードには、それぞれに独自のキーマップがあり、 モード固有のキーマップは、すべてのバッファで共有されるグローバルキーマップを 上書きする。
関数global-set-key
は、グローバルキーマップをバインドする。
たとえば、つぎの式は、
C-c C-lを関数line-to-top-of-window
にバインドする。
(global-set-key "\C-c\C-l" 'line-to-top-of-window)
モード固有のキーマップのバインドには関数define-key
を使う。
この関数は、キーとコマンドに加えて、引数としてモード固有のキーマップを取る。
たとえば、筆者の.emacs
ファイルには、
コマンドtexinfo-insert-@group
をC-c C-c gにバインドする式がある。
(define-key texinfo-mode-map "\C-c\C-cg" 'texinfo-insert-@group)
関数texinfo-insert-@group
自体は、Texinfoモードを少々拡張するもので、
Texinfoファイルに@group
を挿入する。
筆者はこのコマンドをいつも使っており、
6文字の@ g r o u pとタイプするかわりに
3文字のC-c C-c gとタイプするほうを好んで使っている
(@group
と対応する@end group
は、
これらが囲むテキストを1ページに収めるようにするコマンドである。
本書の数行に渡る例題は、@group ... @end group
で囲んである)。
関数texinfo-insert-@group
の定義はつぎのとおりである。
(defun texinfo-insert-@group () "Insert the string @group in a Texinfo buffer." (interactive) (beginning-of-line) (insert "@group\n"))
(もちろん、タイプ量を節約するには、 単語を挿入する関数を書くかわりにAbbrebモードを使うこともできる。 筆者は、Texinfoモードの他のキーバインドとも整合性のよいキー列を好む。)
loaddefs.el
には、
c-mode.el
やlisp-mode.el
のような、さまざまなモードのライブラリに
加えて、数多くのdefine-key
式がある。
キーマップについて詳しくは、 Keymapsや See Key Bindings。
Emacs第19版をMITのXウィンドウシステムのもとで使用している場合には、 色を指定できる (これまでの例は、Emacs第18版でも第19版でも動作するはずであるが、 この例は第19版でのみ動作する)。
デフォルトの色は好みに合わないので、自前の指定をする。
筆者の指定は、Xのさまざまな初期化ファイルに書いてある。
.emacs
ファイルには、何をしたかを覚えておくためのメモを書いておく。
;; I use TWM for window manager; ;; my ~/.xsession file specifies: ; xsetroot -solid navyblue -fg white
実際には、XウィンドウのルートはEmacsの一部ではないが、メモ書きをしておく。
;; My ~/.Xresources file specifies: ; XTerm*Background: sky blue ; XTerm*Foreground: white ; emacs*geometry: =80x40+100+0 ; emacs*background: blue ; emacs*foreground: grey97 ; emacs*cursorColor: white ; emacs*pointerColor: white
筆者の.emacs
ファイルの値を設定する式はつぎのとおりである。
;;; Set highlighting colors for isearch and drag (set-face-foreground 'highlight "white" ) (set-face-background 'highlight "slate blue") (set-face-background 'region "slate blue") (set-face-background 'secondary-selection "turquoise") ;; Set calendar highlighting colors (setq calendar-load-hook '(lambda () (set-face-foreground 'diary-face "skyblue") (set-face-background 'holiday-face "slate blue") (set-face-foreground 'holiday-face "white")))
青のさまざまな濃淡は目にやさしく、画面のチラツキを目立たせない。
Emacs第19版のその他の雑多な設定の一部を説明しておく。
(resize-minibuffer-mode 1) (setq resize-minibuffer-mode t)
(setq search-highlight t)
(setq default-frame-alist '((menu-bar-lines . 1) (auto-lower . t) (auto-raise . t)))
; Cursor shapes are defined in ; `/usr/include/X11/cursorfont.h'; ; for example, the `target' cursor is number 128; ; the `top_left_arrow' cursor is number 132. (let ((mpointer (x-get-resource "*mpointer" "*emacs*mpointer"))) ;; If you have not set your mouse pointer ;; then sent it, otherwise leave as is: (if (eq mpointer nil) (setq mpointer "132")) ; top_left_arrow (setq x-pointer-shape (string-to-int mpointer)) (set-mouse-color "white"))
最後に、筆者のお気に入りの機能を説明する。 モード行の変更である。
筆者はネットワークを介して作業することがあるので、
モード行の左端に表示されるEmacs:
をシステム名で置き換えている。
さもないと、どのマシンを使っているのか忘れることがある。
さらに、どのディレクトリにいるのかを忘れないようにデフォルトのディレクトリを
表示し、ポイントの行位置をLine
に続けて表示する。
筆者の.emacs
ファイルはつぎのようになっている。
(setq mode-line-system-identification (substring (system-name) 0 (string-match "\\..+" (system-name)))) (setq default-mode-line-format (list "" 'mode-line-modified "<" 'mode-line-system-identification "> " "%14b" " " 'default-directory " " "%[(" 'mode-name 'minor-mode-alist "%n" 'mode-line-process ")%]--" "Line %l--" '(-3 . "%P") "-%-")) ;; Start with new default. (setq mode-line-format default-mode-line-format)
Infoなどのさまざまなモードが書き換えられるように
デフォルトのモード行を設定する。
行の構成要素は、自ずとわかるであろう。
mode-line-modified
はバッファが編集されたかどうかを表す変数であり、
mode-name
はモードの名称を表すなどである。
"%14b"
は、
(関数buffer-name
を使って)カレントバッファ名を表示する。
「14」は、表示する最大の文字数を指定する。
名前の文字数が少なければ、この文字数になるまで空白文字を追加する。
%[
と%]
は、各再帰編集レベルに応じて鈎括弧の対を表示する。
%n
は、ナロイングしている場合に「Narrow」を表示する。
%P
は、ウィンドウの最下行より上に表示されているバッファの割合や
「Top」、「Bottom」、「All」を表示する
(小文字のp
は、ウィンドウの先頭行より上に表示されているバッファの割合
を表示する)。
%-
は、モード行が適当な長さになるように-
を詰め込む。
Emacs第19.29版から以降では、frame-title-format
を使って
Emacsのフレームに表題を付けられる。
この変数はmode-line-format
と同じ構造である。
モード行の書式は、Mode Line Formatに記載されている。
「Emacsを好きになるにはEmacsが好きである必要はない」を覚えておいてほしい。 読者のEmacsは、デフォルトのEmacsとは、 色表示が異なり、コマンドも異なり、キーも異なるであろう。
一方、「箱から取り出した」ままのカスタマイズしていないEmacsを起動するには つぎのようにタイプする。
emacs -q
初期化ファイル~/.emacs
をロードせずにEmacsを起動する。
デフォルトのままのEmacsである。
何も変わっていない。
GNU Emacsには2つのデバッガ、debug
とedebug
がある。
最初のものは、Emacs内部に組み込まれていていつでも使える。
2番目のものはEmacsに対する拡張であり、
第19版で標準ディストリビューションの一部になった。
どちらのデバッガもDebuggingに詳しく記載されている。 本章では、それぞれの短い例を示すことにする。
debug
1から指定した数までの総和を求める関数定義を書いたとしよう
(まえに説明した関数triangle
である。
説明は、See Decrementing Example)。
しかし、関数定義にはバグがある。
1-
を1=
とタイプミスした。
まちがった定義はつぎのとおりである。
(defun triangle-bugged (number) "Return sum of numbers 1 through NUMBER inclusive." (let ((total 0)) (while (> number 0) (setq total (+ total number)) (setq number (1= number))) ; ここがエラー total))
Infoで読んでいる場合には、いつものようにこの定義を評価できる。
エコー領域にtriangle-bugged
と表示される。
引数4で関数triangle-bugged
を評価してみる。
(triangle-bugged 4)
つぎのようなエラーメッセージが出る。 Symbol's function definition is void: 1=
実際、このような単純なバグならば、このエラーメッセージから 定義を修正するために必要なことがらがわかる。 しかし、何が起こっているのかわからなかったとしよう。
debug-on-error
の値をt
に設定してデバッガを有効にする。
(setq debug-on-error t)
これは、Emacsにつぎにエラーを検出したらデバッガに入るように指示する。
debug-on-error
にnil
を設定すればデバッガを無効にできる。
(setq debug-on-error nil)
debug-on-error
にt
を設定し、つぎの式を評価する。
(triangle-bugged 4)
こんどは、Emacsは、つぎのようなバッファ*Backtrace*
を作成する。
---------- Buffer: *Backtrace* ---------- Signalling: (void-function 1=) (1= number)) (setq number (1= number))) (while (> number 0) (setq total (+ total number)) (setq number (1= number)))) (let ((total 0)) (while (> number 0) (setq total ...) (setq number ...)) total)) triangle-bugged(4) eval((triangle-bugged 4)) eval-last-sexp(nil) * call-interactively(eval-last-sexp) ---------- Buffer: *Backtrace* ----------
(この例は少々整形してある。 デバッガは長い行を折り畳まない。)
バッファ*Backtrace*
は、下から上へ向けて読む。
Emacsが何を行ってエラーに至ったかを教えてくれる。
この場合は、EmacsはC-x C-e(eval-last-sexp
)を対話的に呼び出し、
これはtriangle-bugged
式の評価につながった。
各行はLispインタープリタが、つぎに何を評価したのかを教えてくれる。
バッファの先頭から3行目はつぎのとおりである。
(setq number (1= number))
Emacsはこの式を評価しようとした。 そのために、先頭から2行目に示されたもっとも内側の式を評価しようとした。
(1= number)
これがエラーを起こした場所であり、 先頭の行にはつぎのように表示されている。
Signalling: (void-function 1=)
この誤りを修正して、関数定義を再評価し、再度試せばよい。
Infoで読んでいる場合には、
debug-on-error
にnil
を設定してデバッガを無効にする。
(setq debug-on-error nil)
debug-on-entry
関数に対してdebug
を起動する2番目の方法は、
関数を呼び出したときにデバッガに入ることである。
これにはdebug-on-entry
を呼ぶ。
つぎのようにタイプする。
M-x debug-on-entry RET triangle-bugged RET
続いて、つぎの式を評価する。
(triangle-bugged 5)
Emacsはバッファ*Backtrace*
を作成し、
関数triangle-bugged
を評価し始めたことを伝える。
---------- Buffer: *Backtrace* ---------- Entering: * triangle-bugged(5) eval((triangle-bugged 5)) eval-last-sexp(nil) * call-interactively(eval-last-sexp) ---------- Buffer: *Backtrace* ----------
バッファ*Backtrace*
の中で、dとタイプする。
Emacsはtriangle-bugged
の最初の式を評価し、
バッファはつぎのようになる。
---------- Buffer: *Backtrace* ---------- Beginning evaluation of function call form: * (let ((total 0)) (while (> number 0) (setq total ...) (setq number ...)) total)) triangle-bugged(5) * eval((triangle-bugged 5)) eval-last-sexp(nil) * call-interactively(eval-last-sexp) ---------- Buffer: *Backtrace* ----------
さらにdをゆっくりと8回タイプする。 dをタイプするたびに、Emacsは関数定義内の別の式を評価する。 最終的に、バッファはつぎのようになる。
---------- Buffer: *Backtrace* ---------- Beginning evaluation of function call form: * (setq number (1= number))) * (while (> number 0) (setq total (+ total number)) (setq number (1= number)))) * (let ((total 0)) (while (> number 0) (setq total ...) (setq number ...)) total)) triangle-bugged(5) * eval((triangle-bugged 5)) eval-last-sexp(nil) * call-interactively(eval-last-sexp) ---------- Buffer: *Backtrace* ----------
最後にさらにdを2回タイプすると、
Emacsはエラーに到達し、バッファ*Backtrace*
の先頭の2行には
つぎのように表示される。
---------- Buffer: *Backtrace* ---------- Signalling: (void-function 1=) * (1= number)) ... ---------- Buffer: *Backtrace* ----------
dをタイプすることで、関数を順を追って実行することができるのである。
バッファ*Backtrace*
を終えるには、qとタイプする。
これは、関数の追跡を止めるが、debug-on-entry
を取り消さない。
debug-on-entry
の効果を無効にするには、
つぎのように、cancel-debug-on-entry
を呼んで関数名を与える。
M-x cancel-debug-on-entry RET triangle-debugged RET
(Infoで読んでいる場合には、ここでdebug-on-entry
を取り消す。)
debug-on-quit
と(debug)
debug-on-error
を設定したりdebug-on-entry
を呼んだりすることに
加えて、debug
を起動するには別に2つの方法がある。
変数debug-on-quit
をt
に設定すると、
C-g(keyboard-quit
)とタイプすると
いつでもdebug
を起動できる。
これは、無限ループのデバッグに役立つ。
あるいは、つぎのように、コード内のデバッガを起動したい場所に(debug)
を
入れておくことである。
(defun triangle-bugged (number) "Return sum of numbers 1 through NUMBER inclusive." (let ((total 0)) (while (> number 0) (setq total (+ total number)) (debug) ; デバッガを起動 (setq number (1= number))) ; ここがエラー total))
関数debug
は、Debuggerに詳しく記載されている。
edebug
Edebugはデバッグ中のソースコードを表示して、 現在評価中の行の左端に矢印を表示する。
関数の実行を行ごとに制御したり、実行を停止する ブレークポイント(breakpoint)まで素早く実行させたりできる。
Edebugは、edebug に記載されている。
バグのあるtriangle-recursively
の関数定義をつぎに示す。
復習するには、See Recursive triangle function。
以下で説明するが、この例ではdefun
の字下げを書いてない。
(defun triangle-recursively-bugged (number) "Return sum of numbers 1 through NUMBER inclusive. Uses recursion." (if (= number 1) 1 (+ number (triangle-recursively-bugged (1= number))))) ; ここがエラー
この定義をインストールするには、普通は、関数の閉じ括弧の直後にカーソルを
置いてC-x C-e(eval-last-sexp
)とタイプするか、
あるいは、定義の途中にカーソルを置いてC-M-x(eval-defun
)
とタイプする
(デフォルトでは、コマンドeval-defun
は、Emacs Lispモードか
Lisp Interactiveモードのみで使える)。
しかし、この関数定義をEdebugで使うには準備する必要があり、 別のコマンドを使ってコードを処置する必要がある。 Emacs第19版では、定義の途中にカーソルを置いてつぎのようにタイプする。
M-x edebug-defun RET
これにより、Emacsは必要ならばEdebugを自動的にロードし、
関数を正しく処置する
(Edebugをロードしたあとは、C-u C-M-x
(前置引数を指定したeval-defun
)などの標準的なキーバインドで
edebug-defun
を呼び出せる)。
Emacs第18版では、読者自身がEdebugをロードする必要がある。
.emacs
ファイルに適切なload
コマンドを入れておく。
Infoで読んでいる場合には、上に示した関数triangle-recursively-bugged
を
処置できる。
edebug-defun
は、defun
の行が字下げされていると
定義の切れ目を検出できない。
そのために、例ではdefun
の左側の空白を取ってある。
関数を処置したら、つぎの式の直後にカーソルを置いて
C-x C-e(eval-last-sexp
)とタイプする。
(triangle-recursively-bugged 3)
triangle-recursively-bugged
のソースに戻り、
カーソルが関数のif
の行の先頭に位置付けされる。
さらに、その行の左端に=>
のような矢印が表示される。
矢印は、関数のどの行を実行中かを示す。
=>-!-(if (= number 1)
<SPC>を押すと、ポイントはつぎに実行すべき式へ移動し、 つぎのようになる。
=>(if -!-(= number 1)
<SPC>を押し続けると、ポイントは式のあいだを移動する。
同時に、式が値を返すと、その値がエコー領域に表示される。
たとえば、number
のあとにポイントが移動したときには、
つぎのように表示される。
Result: 3 = C-c
これは、number
の値は3であり、ASCIIコードでは<CTL-C>
(アルファベットの3番目の文字)であることを意味する。
エラーに達するまで、コードの中を移動できる。 評価するまえには、その行はつぎのようである。
=> -!-(1= number))))) ; ここがエラー
もう一度<SPC>を押すと、つぎのようなエラーメッセージが表示される。
Symbol's function definition is void: 1=
これがバグである。
q
を押してEdebugを終了する。
処置した関数定義を削除するには、 単に処置しない普通のコマンドで関数定義を再評価すればよい。 たとえば、関数定義の閉じ括弧の直後にカーソルを 移動してC-x C-eとタイプする。
Edebugでは、関数の中を動き廻るよりも多くのことができる。 エラーを検出したり指定した停止位置に達した場合だけ停止するとか、 さまざまな式の値の変化を表示したり、 関数が何回呼ばれたかを調べたりなどである。
Edebugはedebugに記載されている。
count-words-region
をインストールし、
これを呼び出すと組み込みのデバッガを起動するようにせよ。
2つの単語を含んだリージョンに対してこのコマンドを実行せよ。
dを何度も押す必要がある。
読者のシステムでは、コマンドが終了したあとに「フック」が呼ばれるか?
(フックについて詳しくは、Command Overviewを参照)
count-words-region
をバッファ*scratch*
にコピーし、
defun
の行のまえの空白を取り去ってから、
Edebug用に関数を処置し、実行を追ってみよ。
この関数にはバグはないはずであるが、必要ならばバグを入れてみよ。
関数にバグがなければ、何の問題にも出会わずに実行を完了する。
global-edebug-prefix
は、普通、C-x Xである。
つまり、<CTL>-xに続けて大文字のXである。
Edebugのバッファ以外でこのキー列を試してみよ)。
count-words-region
がバッファのどのリージョンで
動作しているかをコマンドp(edebug-bounce-point
)を
使って調べてみよ。
edebug-goto-here
)と
タイプしてその場所へジャンプしてみよ。
edebug-trace-mode
)コマンドを使ってみよ。
edebug-Trace-fast-mode
を使うには大文字のTを使う。
これで、本書の終わりまできた。
値を設定する、自分のために簡単な.emacs
ファイルを書く、
Emacsの簡単なカスタマイズや拡張を書くなどのEmacs Lispでのプログラミングに
ついて十分に学べたものと思う。
これで終わりかもしれない。 あるいは、さらに進んで、自習することもあろう。
プログラミングの基本を少しは学べたはずである。 しかし、ほんの少しである。 ここでは説明しなかった多くのことがらがまだまだある。
読者が進むべき道は、GNU Emacsのソースや、
Emacs Lispのソースは冒険である。 ソースを読み進めているときに、不慣れな関数や式に出会った場合には、 それが何をするものか突き止める必要がある。
リファレンスマニュアルに進んだとする。 これは、とおしで完全な比較的読みやすいEmacs Lispの解説である。 経験者向けだけでなく、一般向けでもある (リファレンスマニュアルは、 GNU Emacsの標準ディストリビューションに含まれる。 本書のように、Texinfoのソースファイルが付属しているので、 オンラインでも印刷しても読める)。
GNU Emacsの一部でもあるオンラインヘルプに進むこともできる。
すべての関数にはオンラインヘルプがあり、
find-tags
はソースファイルへ導くプログラムである。
筆者はつぎのようにしてソースを探検した。
だいぶまえに、その名前からまずsimple.el
を最初に見てみた。
simple.el
の関数のいくつかは複雑だったり、少なくとも一見しただけでは
複雑に見えた。
たとえば、最初の関数は複雑に見えた。
それは関数open-line
であった。
関数forward-sentence
でやったように、読者は、この関数をゆっくりと
ウォークスルーするかもしれない
あるいは、この関数を飛ばして、split-line
などの別の関数を
読むかもしれない。
全部の関数を読む必要はない。
count-words-in-defun
によれば、関数split-line
には
27個の単語やシンボルが含まれている。
これは短いが、split-line
には、説明していない4つの式、
skip-chars-forward
、indent-to
、insert
、?\n
が
含まれている。
関数insert
を考えてみよう(これは、
Emacsでは、C-h f(describe-function
)に
続けて関数名をタイプすれば、insert
についてさらに調べることができる。
関数の説明文を表示してくれる。
M-.にバインドされたfind-tag
を使えばソースを見ることもできる
(ここではこれはあまり役立たない。
この関数はLispではなくCで書いた基本操作関数である)。
最後に、リファレンスマニュアルにあるように、
i(Info-index
)に続けて関数名をタイプして
Infoのマニュアルを見るか、印刷したマニュアルの索引からinsert
を探す。
?\n
の意味も同様にして調べられる。
Info-index
に?\n
を指定してみる。
これは役立たないことがわかるであろうが、諦めないでほしい。
?
を付けずに\n
だけで探すと、マニュアルの関連する節に辿りつける
(See Character Type。
?\n
は改行を意味する)。
skip-chars-forward
やindent-to
の動作を予想することもできるし、
探すこともできる
(関数describe-function
自身はhelp.el
にある。
長い関数の1つであるが、読みこなせる関数である。
この定義は、標準のコード文字を使わずにinteractive
式を
カスタマイズする例になる。
一時的なバッファの作成方法の例でもある)。
その他の興味深いファイルは、paragraphs.el
、loaddefs.el
、
loadup.el
である。
ファイルparagraphs.el
には、長いものもあるが、
短くて理解しやすい関数がある。
ファイルloaddefs.el
には、標準的な多数のオートロードや
多数のキーマップがある。
筆者もその一部を見ただけで、すべてを見たことはない。
loadup.el
は、Emacsの標準的なものをロードするためのファイルである。
Emacsの構築方法に関してとても詳しく教えてくれる
(構築に関して詳しくは、See Building Emacs)。
初歩は学べたはずである。
しかし、プログラミングの重要な側面にはふれなかった。
既知の関数sort
を使う以外には、
情報をソートする方法については説明しなかった。
変数やリストを使うことを除いて、情報の保存方法については説明しなかった。
プログラムを作成するプログラムの書き方については説明しなかった。
これらは、別の話題である。
GNU Emacsを実用的に使うに十分なことは学べたと思う。 やっと、始めたばかりである。 これで入門を終える。
the-the
文書を書いているときに、「you you」などと単語をだぶらせることがある。
筆者は、しばしば「the」をだぶって書いていることに気づいた。
そこで、重複した単語を検出する関数をthe-the
と命名する。
第1段階としては、重複の検出にはつぎの正規表現が使える。
\\(\\w+[ \t\n]+\\)\\1
この正規表現は、単語構成文字の1個以上の繰り返しに続いて 空白文字やタブや改行が1個以上続くものに一致する。 しかし、最初の単語の終わりである改行は、2番目の単語の終わりである空白文字とは 異なるので、2行にわたる単語の重複は検出できない (正規表現についてより詳しくは、 Regexp SearchやRegexpsやRegular Expressionsを参照)。
パターンは「with the」の「th」のような重複を検出できないので、 単語構成文字の重複の探索を試しても失敗する。
別の正規表現では、単語構成文字に続けて単語を構成しない文字があり重複部分
が続くようなものを探す。
ここで、\\w+
は、単語構成文字の1個以上の繰り返しに一致し、
\\W*
は、単語を構成しない文字の0個以上の繰り返しに一致する。
\\(\\(\\w+\\)\\W*\\)\\1
これも、役に立たない。
つぎに、筆者が用いているパターンを示す。
完全ではないが十分である。
\\b
は、単語の始めや終わりにある空の文字列に一致する。
[^@ \n\t]+
は、@-記号、空白文字、改行、タブ以外の
任意の文字の1個以上の繰り返しに一致する。
\\b\\([^@ \n\t]+\\)[ \n\t]+\\1\\b
より完全な表現を書くことも可能であろうが、 この表現でも十分なのでこれを使っている。
グローバルキーバインドとともに.emacs
ファイルに収めた
関数the-the
をつぎに示す。
(defun the-the () "Search forward for for a duplicated word." (interactive) (message "Searching for for duplicated words ...") (push-mark) ;; This regexp is not perfect ;; but is fairly good over all: (if (re-search-forward "\\b\\([^@ \n\t]+\\)[ \n\t]+\\1\\b" nil 'move) (message "Found duplicated word.") (message "End of buffer"))) ;; Bind `the-the' to C-c \ (global-set-key "\C-c\\" 'the-the)
テスト用のテキストを示しておく。
one two two three four five five six seven
上の関数定義の正規表現を別の正規表現に置き換えることもでき、 それぞれをこのテキストで試すとよい。
キルリングはリストであるが、
関数rotate-yank-pointer
の働きでリングに変換されている。
コマンドyank
とyank-pop
は、
関数rotate-yank-pointer
を使っている。
本付録では、関数rotate-yank-pointer
とともに、
コマンドyank
とyank-pop
を説明する。
rotate-yank-pointer
関数rotate-yank-pointer
は、kill-ring-yank-pointer
が指す
キルリングの要素を変更する。
たとえば、kill-ring-yank-pointer
が第2要素を指していたら
第3要素を指すように変更する。
rotate-yank-pointer
のコードをつぎに示す。
(defun rotate-yank-pointer (arg) "Rotate the yanking point in the kill ring." (interactive "p") (let ((length (length kill-ring))) (if (zerop length) ;; 真の場合の動作 (error "Kill ring is empty") ;; 偽の場合の動作 (setq kill-ring-yank-pointer (nthcdr (% (+ arg (- length (length kill-ring-yank-pointer))) length) kill-ring)))))
関数は複雑に見えるが、いつものように、部分部分に分解すれば理解できる。 まず、骨格から見てみよう。
(defun rotate-yank-pointer (arg) "Rotate the yanking point in the kill ring." (interactive "p") (let 変数リスト 本体...)
この関数は、1つの引数、arg
を取る。
短い説明文字列があり、小文字のp
を指定したinteractive
がある。
これは、前置引数を処理した数を引数として関数に渡すことを意味する。
関数定義の本体はlet
式であり、これには変数リストと本体がある。
let
式は、この関数内部でのみ使える変数を宣言する。
この変数は、length
であり、キルリング中の要素数に束縛される。
関数length
を呼び出してこれを行う
(この関数は変数length
と同じ名前である。
しかし、一方は関数の名称としての使い方であり、
他方は変数の名称としての使い方である。
この2つはまったく異なる。
同様に、英語の話者は、
『I must ship this package immediately.』と
『I must get aboard the ship immediately.』とのship
の意味を
区別できる)。
関数length
は、リスト内の要素の個数を返すので、
(length kill-ring)
は、キルリングにある要素の個数を返す。
rotate-yank-pointer
.
rotate-yank-pointer
の本体rotate-yank-pointer
の本体はlet
式であり、
let
式の本体はif
式である。
if
式の目的は、キルリングに何かがあるかどうかを調べることである。
キルリングが空ならば、関数error
が関数の評価を停止し、
エコー領域にメッセージを表示する。
一方、キルリングに何かがあれば、関数の処理を実施する。
if
式の判定条件と真の場合の動作をつぎに示す。
(if (zerop length) ; 判定条件 (error "Kill ring is empty") ; 真の場合の動作 ...
キルリングに何もなければ、その長さは0であるはずなので、
ユーザーにエラーメッセージKill ring is empty
を表示する。
if
式では関数zerop
を用いるが、これは値が0ならば真を返す。
zerop
が真を返したならば、if
の真の場合の動作が評価される。
真の場合の動作は、関数error
で始まるリストである。
これは関数message
(see message)に似ており、
1行のメッセージをエコー領域に表示する。
しかし、メッセージの表示に加えて、
error
はこれを含んだ関数の評価を停止させる。
この場合は、キルリングの長さが0ならば、
関数の残りの部分は評価されないことを意味する。
(筆者の意見では、この関数の名前に「error」を使うことは、 少なくとも人間にとっては、少々誤解を招く。 よりよい名前は「cancel」であろう。 厳密にいえば、長さが0のリストを指すポインタをつぎの要素を 指すように巡回できないので、コンピュータの視点からは 「error」は正しい用語である。 しかし、キルリングが満杯か空かを調べるだけでも、 人間はこの種のことを試せるものと考える。 これは、探査行為である。)
(人間の視点からは、探査行為や発見行為は、必ずしもエラーではなく、 したがって、たとえコンピュータ内部であっても、「error」と呼ぶべきではない。 Emacsのコードでは、高潔に振舞っている人間がその環境を探査していて エラーを引き起こしたということになる。 これは残念である。 エラーがあった場合にはコンピュータは同じ手順を踏むのであるが、 「cancel」のような用語のほうが、はっきりと意味を表す。)
if
expression.
%
, function.
%
in rotate-yank-pointer
.
if
式の偽の場合の動作if
式の偽の場合の動作は、キルリングに何かがあるときに
kill-ring-yank-pointer
の値を設定することに費される。
コードはつぎのとおりである。
(setq kill-ring-yank-pointer (nthcdr (% (+ arg (- length (length kill-ring-yank-pointer))) length) kill-ring)))))
これには説明が必要であろう。
明らかに、kill-ring-yank-pointer
には、
前節で説明した関数nthcdr
を使って
キルリングのどこかのCDRを設定する(See copy-region-as-kill)。
これをどうやって行っているのであろう?
コードの詳細を見るまえに、関数rotate-yank-pointer
の目的を考えてみよう。
関数rotate-yank-pointer
は、
kill-ring-yank-pointer
が指すものを変更する。
kill-ring-yank-pointer
がリストの先頭要素を指している場合に
rotate-yank-pointer
を呼び出すと、2番目の要素を指すように変わる。
kill-ring-yank-pointer
が2番目の要素を指している場合に
rotate-yank-pointer
を呼び出すと、3番目の要素を指すように変わる
(また、rotate-yank-pointer
に1より大きな引数を与えると、
その数だけポインタを進める)。
関数rotate-yank-pointer
は、
kill-ring-yank-pointer
が指すものを再設定するためにsetq
を使う。
kill-ring-yank-pointer
がキルリングの第1要素を指している場合、
もっとも簡単な場合であるが、関数rotate-yank-pointer
は、
kill-ring-yank-pointer
が2番目の要素を指すようにする。
いいかえれば、kill-ring-yank-pointer
には、キルリングのCDRと
同じ値が設定される必要がある。
つまり、つぎのような場合、
(setq kill-ring-yank-pointer ("some text" "a different piece of text" "yet more text")) (setq kill-ring ("some text" "a different piece of text" "yet more text"))
コードはつぎのようにする必要がある。
(setq kill-ring-yank-pointer (cdr kill-ring))
その結果、kill-ring-yank-pointer
はつぎのようになる。
kill-ring-yank-pointer => ("a different piece of text" "yet more text"))
実際のsetq
式では関数nthcdr
を使ってこれを行う。
すでに見たように(see nthcdr)、
関数nthcdr
は、リストのCDRを繰り返し取る。
CDRのCDRのCDR ...を取る。
つぎの2つの式は同じ結果になる。
(setq kill-ring-yank-pointer (cdr kill-ring)) (setq kill-ring-yank-pointer (nthcdr 1 kill-ring))
しかし、関数rotate-yank-pointer
では、nthcdr
の第1引数は、
多くの算術を含んだ複雑に見える式である。
(% (+ arg (- length (length kill-ring-yank-pointer))) length)
いつものように、もっとも内側の式から始めて、外側に向かって調べる必要がある。
もっとも内側の式は、(length kill-ring-yank-pointer)
である。
これは、kill-ring-yank-pointer
の現在の長さを調べる
(kill-ring-yank-pointer
は、その値がリストである変数の名前である)。
この長さは、つぎの式の内側にある。
(- length (length kill-ring-yank-pointer))
この式では、length
は、関数の始めにあるlet
文にて
キルリングの長さを設定した変数である
(変数の名前をlength
ではなくlength-of-kill-ring
とすれば、
この関数がより明確になると考える読者がいるかもしれない。
しかし、関数のテキスト全体を見れば短いので、
ここでやっているように関数を小さな部分部分に分解しなければ、
変数をlength
と命名しても邪魔ではない)。
したがって、(- length (length kill-ring-yank-pointer))
は、
キルリングの長さとkill-ring-yank-pointer
のリストの長さの差を取る。
これが関数rotate-yank-pointer
にどのように適合するのかを、
つぎの状況で分析してみよう。
kill-ring
と同じでkill-ring-yank-pointer
は
キルリングの第1要素を指し、
rotate-yank-pointer
を引数1で呼び出したとする。
この場合、変数length
はキルリングの長さであり、
kill-ring-yank-pointer
はキルリング全体を指しているので、
変数length
と式(length kill-ring-yank-pointer)
の値は同じである。
そのため、
(- length (length kill-ring-yank-pointer))
の値は0になる。
arg
は1なので、式全体では
(+ arg (- length (length kill-ring-yank-pointer)))
の値は1になる。
したがって、nthcdr
の引数は、つぎの式の結果である。
(% 1 length)
%
(% 1 length)
を理解するには%
を理解する必要がある。
(C-h f <%> <RET>とタイプして探した)その説明文によれば、
関数%
は、第1引数を第2引数で割ったときの余りを返す。
たとえば、5を2で割った余りは1である
(2を2倍して余り1を加えると5になる)。
算術をほとんどしない人には、 小さな数を大きな数で割ることができて余りがあることに驚くかもしれない。 例では、5を2で割った。 逆に、2を5で割った結果はどうなるであろう? 小数を使えば、答えは、2/5、つまり、0.4である。 しかし、整数しか使えない場合には、結果は異なったものになる。 明らかに、5を0倍すればよいだろうが、余りはいくつだろう? 答えを探すには、子どものころから馴染み深い場合分けを考える。
これに対比させると、
などなどである。
したがって、このコードでは、length
の値は5なので、
(% 1 5)
を評価した結果は1である (式の直後にカーソルを置いてC-x C-eとタイプして確認した。 もちろん、エコー領域には1と表示される)。
rotate-yank-pointer
における%
の利用kill-ring-yank-pointer
がキルリングの先頭を指し、
rotate-yank-pointer
に渡した引数が1の場合には、
%
式は1を返す。
(- length (length kill-ring-yank-pointer)) => 0
したがって、
(+ arg (- length (length kill-ring-yank-pointer))) => 1
であり、length
の値に関係なく、
(% (+ arg (- length (length kill-ring-yank-pointer))) length) => 1
となる。
この結果、式setq kill-ring-yank-pointer
はつぎのように簡約できる。
(setq kill-ring-yank-pointer (nthcdr 1 kill-ring))
この動作を理解するのは簡単である。
キルリングの第1要素を指していたものが、kill-ring-yank-pointer
は
第2要素を指すように設定される。
明らかに、rotate-yank-pointer
に渡す引数が2の場合、
kill-ring-yank-pointer
には(nthcdr 2 kill-ring)
が設定される。
引数に他の値を指定すると変わる。
同様に、kill-ring-yank-pointer
がキルリングの第2要素を
指している状態では、その長さはキルリングの長さより1小さいので、
余りの計算は式(% (+ arg 1) length)
をもとに行われる。
つまり、rotate-yank-pointer
へ渡す引数が1ならば、
kill-ring-yank-pointer
は、キルリングの第2要素から第3要素に移動する。
最後の疑問は、kill-ring-yank-pointer
がキルリングの最後の
要素を指しているとどうなるかである。
rotate-yank-pointer
を呼び出すと、
キルリングからは何も取り出せないのであろうか?
答えは否である。
これとは異なる有用なことが起こる。
kill-ring-yank-pointer
は、キルリングの先頭を指すように設定される。
これがどのように行われるかを、キルリングの長さを5、
rotate-yank-pointer
に渡す引数を1と仮定して、コードを見てみよう。
kill-ring-yank-pointer
がキルリングの最後の要素を指していると、
その長さは1である。
コードはつぎのとおりであった。
(% (+ arg (- length (length kill-ring-yank-pointer))) length)
変数に数値を入れると、式はつぎのようになる。
(% (+ 1 (- 5 1)) 5)
もっとも内側の式から外側に向かって評価する。
(- 5 1)
の値は4、(+ 1 4)
の合計は5、
5を5で割った余りは0である。
したがって、rotate-yank-pointer
が実行することは
(setq kill-ring-yank-pointer (nthcdr 0 kill-ring))
であり、kill-ring-yank-pointer
はキルリングの先頭を指すように設定される。
rotate-yank-pointer
を連続して呼び出すと、
キルリングの最後に達するまでは、
kill-ring-yank-pointer
はキルリングの要素を順番に指し、先頭に戻る。
リストには終わりがないかのように先頭に戻るので、
キルリングをリングとよぶのである
(リングとは、終わりがないもの?)。
yank
rotate-yank-pointer
を理解していれば、関数yank
は簡単である。
唯一の巧妙な部分は、rotate-yank-pointer
に渡す引数を計算する部分である。
コードはつぎのとおりである。
(defun yank (&optional arg) "Reinsert the last stretch of killed text. More precisely, reinsert the stretch of killed text most recently killed OR yanked. With just C-U as argument, same but put point in front (and mark at end). With argument n, reinsert the nth most recently killed stretch of killed text. See also the command \\[yank-pop]." (interactive "*P") (rotate-yank-pointer (if (listp arg) 0 (if (eq arg '-) -1 (1- arg)))) (push-mark (point)) (insert (car kill-ring-yank-pointer)) (if (consp arg) (exchange-point-and-mark)))
このコードをざっと見ると、最後の数行はすぐにわかりそうである。
マークをプッシュする、つまり、記録する。
kill-ring-yank-pointer
が指す最初の要素(CAR)は挿入するものである。
関数に渡された引数がcons
ならば、ポイントとマークを交換して、
挿入したテキストの最後ではなく先頭にポイントを移動する。
このオプションは説明文で解説してある。
関数自体は、"*P"
を指定した対話的なものである。
これは、読み出し専用のバッファでは動作せず、
未処理の前置引数を関数に渡すことを意味する。
rotate-yank-pointer
.
yank
の難しい部分は、rotate-yank-pointer
に渡す引数を決定する
ための計算を理解することである。
幸い、一見したほど難しくはない。
一方あるいは両方のif
式を評価すると数になり、
その数がrotate-yank-pointer
に渡される引数となる。
注釈を付けると、コードはつぎのようになる。
(if (listp arg) ; 判定条件 0 ; 真の場合の動作 (if (eq arg '-) ; 偽の場合の動作、内側のif -1 ; 内側のifの真の場合の動作 (1- arg)))) ; 内側のifの偽の場合の動作
このコードは2つのif
式から成り、
一方の偽の場合の動作に他方が含まれている。
最初の、つまり、外側のif
式では、yank
に渡された引数が
リストかどうかを調べる。
奇妙であるが、引数なしでyank
が呼ばれると、これは真になる。
省略できる引数に渡される値はnil
であり、
(listp nil)
を評価すると真を返すからである。
そこで、yank
に引数を渡さないと、yank
の中の
rotate-yank-pointer
に渡す引数は0である。
つまり、予想どおりに、ポインタは移動されずに、kill-ring-yank-pointer
が
指す先頭要素が挿入される。
同様に、yank
への引数がC-uであると、これはリストとして読まれ、
この場合もrotate-yank-pointer
には0が渡される
(C-uは、未処理の前置引数である(4)
となり、
これは1要素のリストである)。
同時に、関数のうしろの部分で、この引数はcons
であることがわかるので、
ポイントを挿入したテキストの始まりに、
マークを挿入したテキストの終わりに移動する
(interactive
の引数P
は、省略可能な引数が略されていたり、
C-uだったりした場合にこれらの値を提供するためのものである)。
外側のif
式の真の場合の動作では、引数がなかったりC-uであった
場合の処理を行う。
偽の場合の動作では、それ以外の状況を処理する。
偽の場合の動作自身は、別のif
式である。
内側のif
式では、引数が負かどうかを調べる
(<META>と<->キーを同時に押し下げるか、
<ESC>キーに続けて<->キーを押すとこのようになる)。
この場合には、関数rotate-yank-pointer
には、
引数として-1が渡される。
そうするとkill-ring-yank-pointer
は逆向きに移動し、望んだ動作となる。
内側のif
の判定条件が偽(つまり、引数はマイナス記号ではない)であると、
式の偽の場合の動作が評価される。
これは式(1- arg)
である。
2つのif
式のために、引数が正の数か(マイナス記号だけではない)負の数の
場合にこの式が評価される。
(1- arg)
は、引数から1を引いた値を返す
(1-
関数は、引数から1を引く)。
つまり、yank
への引数が1ならば、0に減らされ、
予想どおりに、
kill-ring-yank-pointer
が指す先頭要素が挿入される。
最後に、剰余関数%
や関数nthcdr
に負の引数を与えると
どうなるのであろうか?
試してみればわかる。
(% -1 5)
を評価すると、負の数が返される。
負の数でnthcdr
を呼び出すと、第1引数が0の場合と同じ値を返す。
つぎのコードを評価するとこのことがわかる。
=>
のまえの式を評価すると=>
のあとに
示した結果になる。
いつものように、コードの直後にカーソルを置いてC-x C-e
(eval-last-sexp
)とタイプして行った。
GNU EmacsのInfoで読んでいる場合には、読者自身で試してほしい。
(% -1 5) => -1 (setq animals '(cats dogs elephants)) => (cats dogs elephants) (nthcdr 1 animals) => (dogs elephants) (nthcdr 0 animals) => (cats dogs elephants) (nthcdr -1 animals) => (cats dogs elephants)
したがって、yank
にマイナス記号や負の数を渡すと、
kill-ring-yank-pointer
は先頭に達するまで逆向きに巡回される。
そして、先頭で止まる。
リストの最後に達するとリストの先頭へ戻ってリングを形成するのとは異なり、
先頭で止まる。
これには意味がある。
なるべく最近に切り取ったテキストの断片を戻したいとしばしば思うだろうが、
30回もまえのキルコマンドからテキストを挿入したくはないであろう。
そこで、終わりに向かってはリングである必要があるが、
逆向きで先頭に戻った場合には巡回しない。
マイナス記号付きのどんな数をyank
に渡しても、-1と解釈される。
これはプログラムの記述を明らかに簡単にする。
キルリングの先頭へ向けて一度に1よりも大きく戻る必要はないので、
マイナス記号に続く数の大きさを調べるように関数を書くよりもよほど簡単である。
yank-pop
yank
を理解していれば、関数yank-pop
は簡単である。
場所を節約するために説明文を省くと、つぎのようになる。
(defun yank-pop (arg) (interactive "*p") (if (not (eq last-command 'yank)) (error "Previous command was not a yank")) (setq this-command 'yank) (let ((before (< (point) (mark)))) (delete-region (point) (mark)) (rotate-yank-pointer arg) (set-mark (point)) (insert (car kill-ring-yank-pointer)) (if before (exchange-point-and-mark))))
この関数には小文字のp
を指定したinteractive
があるので、
前置引数は処理してから関数に渡される。
コマンドはyankの直後でのみ使え、それ以外の場合にはエラーメッセージが送られる。
この検査には、すでに説明した変数last-command
を使っている
(See copy-region-as-kill)。
let
節では、ポイントがマークのまえにあるかうしろにあるかに依存して
変数before
に真か偽を設定し、
ポイントとマークのあいだのリージョンを削除する。
これは直前のyankで挿入したリージョンであり、
この部分のテキストを置き換えるのである。
つぎに、kill-ring-yank-pointer
を巡回して、
直前に挿入したテキストを再度挿入しないようにする。
新たに挿入するテキストの先頭にマークを設定し、
kill-ring-yank-pointer
が指す先頭要素をポイントに挿入する。
これにより、テキストの終わりにポイントが置かれる。
直前のyankで挿入したテキストの先頭にポイントを置いてあった場合には、
ポイントとマークを入れ換えて、新たに挿入したテキストの先頭に
再度ポイントを置く。
以上である。
座標軸が表示されていると、グラフの理解に役立つ。 これらは尺度を与えてくれる。 前節(see Readying a Graph)では、 グラフの本体を表示するコードを書いた。 ここでは、本体自身に加えて、ラベルを付けた垂直軸と水平軸も 表示するコードを書く。
let
expression in print-graph
.
バッファへの挿入はポイントの右下へ向かって行われるので、 グラフを描く新しい関数では、まず、Y軸、つまり、垂直軸を描いてから、 グラフの本体を描き、最後に、X軸、つまり、水平軸を描く。 この描く順序から、関数の内容が決まる。
最終的なグラフはつぎのようになる。
10 - * * * * ** * *** 5 - * ******* * *** ******* ************* *************** 1 - **************** | | | | 1 5 10 15
このグラフでは、垂直と水平の両者の軸に、数字のラベルを付けてある。 しかし、ある種のグラフでは、水平軸は時間であり、つぎのように、 月名のラベルのほうが適することもある。
5 - * * ** * ******* ********** ** 1 - ************** | ^ | Jan June Jan
もちろん、少々考えれば、垂直/水平軸のさまざまなラベリング方法を考えつく。 われわれの仕事も複雑になる。 複雑さは混乱を招く。 したがって、まずは単純なラベリング方法を選び、 そのあとで修正なり置き換えを行う。
以上の考察から、関数print-graph
の概略はつぎのようになる。
(defun print-graph (numbers-list) "説明文..." (let ((height ... ...)) (print-Y-axis height ... ) (graph-body-print numbers-list) (print-X-axis ... )))
print-graph
の関数定義の各部分を順番に扱おう。
print-graph
の変数リスト関数print-graph
を書く場合、最初の仕事は、
let
式の変数リストを書くことである
(関数を対話的にしたり、説明文字列の内容については、
しばらく手をつけないでおく)。
変数リストでは、数個の変数を設定する。
明らかに、垂直軸のラベルの先頭は少なくともグラフの高さである必要があるので、
この情報をここで得ておく必要がある。
関数print-graph-body
もこの情報を必要とすることに注意してほしい。
異なる2つの場所でグラフの高さを計算する理由はないので、
ここでの計算を利用するようにprint-graph-body
の以前の定義を変更する。
同様に、X軸を描く関数と関数print-graph-body
の両者は、
各シンボルの幅を知る必要がある。
この計算もここで行い、print-graph-body
の以前の定義を変更する。
水平軸のラベルの長さは少なくともグラフの幅である必要がある。 しかし、この情報は水平軸を描く関数でのみ使われるので、 ここで計算する必要はない。
以上の考察から、print-graph
のlet
の変数リストはつぎのようになる。
(let ((height (apply 'max numbers-list)) ; 第1版 (symbol-width (length graph-blank)))
あとでわかるように、この式は実は正確ではない。
print-Y-axis
関数print-Y-axis
の仕事は、つぎのような垂直軸のラベルを描くことである。
10 - 5 - 1 -
関数にはグラフの高さが渡され、適切な数字や目盛を作成して挿入する。
Y軸のラベルをどのようにすべきかは図からは簡単にわかるが、
言葉で書き表したり、そのようにする関数定義を書くことはまた別である。
5行ごとに数字や目盛が必要なのではない。
1
と5
のあいだには3行(2行目、3行目、4行目)あるが、
5
と10
のあいだには4行(6行目、7行目、8行目、9行目)ある。
基準行(数1)には数字と目盛が必要であり、最下行から5行目および5の倍数の行に
数字と目盛が必要であるといい直したほうがよい。
つぎの問題は、ラベルの高さをどうすべきかである。
グラフのもっとも大きな高さが7であったとしよう。
Y軸のもっとも大きなラベルを5 -
にして、
グラフの棒がラベルより高くなってもよいのだろうか?
あるいは、もっとも大きなラベルを7 -
にして、
グラフの最大値の目印にするのだろうか?
あるいは、もっとも大きなラベルを5の倍数の10 -
にして、
グラフの最大値よりも高くするのだろうか?
後者が望ましい。
ほとんどのグラフは、刻み幅の倍数の長さの辺の四角形の中に書かれる。
刻み幅が5であれば、辺の長さは、5、10、15、などなどである。
垂直軸の高さを刻み幅の倍数にしようとすると、
変数リストの高さを計算する単純な式はまちがっていることに気づく。
式は(apply 'max numbers-list)
であった。
これは、正確に最大の高さを返し、
5の倍数となるような最大数に繰り上げた値を返さない。
より複雑な式が必要である。
このような場合の常として、小さな複数の問題に分解できれば、 複雑な問題は簡単になる。
まず、グラフの最大の高さが5の倍数、つまり5、10、15、などの場合を考える。 この場合には、この値をY軸の高さとして使える。
数が5の倍数であるかを調べる比較的簡単な方法は、 数を5で割って余りを調べることである。 余りがなければ、数は5の倍数である。 つまり、7を5で割ると余りは2となり、7は5の倍数ではない。 いい方を変えて教室でのことを思い出すと、5を1倍して余り2を足すと7になる。 しかし、5を2倍して余り0を足すと10になり、10は5の倍数である。
Lispでは、余りを計算する関数は%
である。
この関数は、第1引数を第2引数で割った余りを返す。
%
は、apropos
を使って探せないEmacs Lispの関数である。
M-x apropos <RET> remainder <RET>とタイプしても何も得られない。
%
の存在を知る唯一の方法は、本書のような書籍を読むか、
Emacs Lispのソースを読むことである。
関数%
は、付録で説明しているrotate-yank-pointer
のコードで
使われている(See rotate-yk-ptr body)。
つぎの2つの式を評価すれば関数%
を試せる。
(% 7 5) (% 10 5)
最初の式は2を返し、2番目は0を返す。
返された値が0かどうかを調べるには、関数zerop
を使う。
この関数は、数である引数が0ならばt
を返す。
(zerop (% 7 5)) => nil (zerop (% 10 5)) => t
したがって、つぎの式はグラフの高さが5の倍数ならばt
を返す。
(zerop (% height 5))
(もちろん、height
の値は、(apply 'max numbers-list)
の値である。)
一方、height
の値が5の倍数でなかったら、
それより大きなつぎの5の倍数に直したいのである。
これは、すでに馴染みのある関数を使った単純な算術でできる。
まず、height
の値を5で割って、5の何倍かを調べる。
したがって、12は、5の2倍はある。
この商に1を加えて5を掛けると、高さより大きなつぎの5の倍数の値を得られる。
12は、5の2倍はある。
2に1を加えて、5を掛ける。
結果は15であり、これは12より大きなつぎの5の倍数である。
これに対応するLispの式はつぎのとおりである。
(* (1+ (/ height 5)) 5)
たとえば、つぎの式を評価すると結果は15である。
(* (1+ (/ 12 5)) 5)
これまでの説明では、Y軸の刻み幅として5を使ってきたが、
これ以外の値を使いたい場合もあろう。
一般的には、5のかわりに、値を設定できる変数を使うべきである。
この変数の名前としてはY-axis-label-spacing
が最適であると思う。
これを使うと、if
式はつぎのようになる。
(if (zerop (% height Y-axis-label-spacing)) height ;; そうでなければ (* (1+ (/ height Y-axis-label-spacing)) Y-axis-label-spacing))
この式は、高さがY-axis-label-spacing
の値の倍数ならばheight
の
値を返し、そうでなければ、
Y-axis-label-spacing
のつぎに大きな倍数を計算してその値を返す。
(Y-axis-label-spacing
の値を設定してから)
この式を関数print-graph
のlet
式に埋め込む。
(defvar Y-axis-label-spacing 5 "Number of lines from one Y axis label to next.") ... (let* ((height (apply 'max numbers-list)) (height-of-top-line (if (zerop (% height Y-axis-label-spacing)) height ;; そうでなければ (* (1+ (/ height Y-axis-label-spacing)) Y-axis-label-spacing))) (symbol-width (length graph-blank)))) ...
(関数let*
を使っていることに注意してほしい。
高さの初期値を、いったん、式(apply 'max numbers-list)
で計算し、
最終的な値を計算するためにheight
の値を使っている。
let*
について詳しくは、See fwd-para let。)
垂直軸を書くときには、5 -
や10 -
などの文字列を
5行ごとに書きたい。
さらに、数字と目盛をきちんとそろえたいので、
短い数字には空白をまえに埋める必要がある。
数字を2桁で表す文字列がある場合には、
数字が1桁になる文字列では、数字の直前に空白文字を埋める必要がある。
数の桁数を調べるには、関数length
を使う。
しかし、関数length
は文字列のみを扱い、数を扱えない。
そのため、数を文字列に変換する必要がある。
これは関数int-to-string
で行う。
たとえば、
(length (int-to-string 35)) => 2 (length (int-to-string 100)) => 3
さらに、各ラベルの数字のあとには -
などの文字列が続く。
これをY-axis-tic
と呼ぶことにする。
この変数をつぎのようにdefvar
で定義する。
(defvar Y-axis-tic " - " "String that follows number in a Y axis label.")
Y軸のラベルの長さは、Y軸の目盛の長さとグラフの先頭の数字の桁数の総和である。
(length (concat (int-to-string height) Y-axis-tic)))
この値は、関数print-graph
の変数リストでfull-Y-label-width
として
計算する
(始めは、これを変数リストに含めるとは思っていなかった)。
垂直軸の完全なラベルを作るには、数字に目盛を繋げ、数字の桁数に応じて
それらのまえに空白文字を繋げる。
ラベルは3つの部分、(省略されるかもしれない)空白、数字、目盛から成る。
関数には、特定の行の数の値と、print-graph
で(一度だけ)計算された
先頭行の幅が渡される。
(defun Y-axis-element (number full-Y-label-width) "Construct a NUMBERed label element. A numbered element looks like this ` 5 - ', and is padded as needed so all line up with the element for the largest number." (let* ((leading-spaces (- full-Y-label-width (length (concat (int-to-string number) Y-axis-tic))))) (concat (make-string leading-spaces ? ) (int-to-string number) Y-axis-tic)))
関数Y-axis-element
は、必要ならばまえに置く空白、
文字列にした数字、目盛を繋げる。
ラベルに何個の空白が必要かを調べるために、目的とするラベルの幅から、 数字の桁数と目盛の長さを足した実際の目盛の長さを引く。
関数make-string
を使って、空白文字を挿入する。
この関数は2つの引数を取る。
第1引数で文字列の長さを指定し、
第2引数には挿入する文字を特別な形式で指定する。
ここでは、?
のように疑問符のあとに空白文字を続ける。
文字の書き方に関する記述は、
See Character Type。
関数int-to-string
は、文字列を繋げる式で使われており、
数を文字列に変換する。
この文字列には、まえに置く空白文字や目盛が繋げられる。
これまでの関数は、 垂直軸のラベルとして挿入する数字や空白から成る文字列のリストを 生成する関数を作るために必要なすべての道具を提供する。
(defun Y-axis-column (height width-of-label) "Construct list of Y axis labels and blank strings. For HEIGHT of line above base and WIDTH-OF-LABEL." (let (Y-axis) (while (> height 1) (if (zerop (% height Y-axis-label-spacing)) ;; ラベルを挿入する (setq Y-axis (cons (Y-axis-element height width-of-label) Y-axis)) ;; そうでなければ、空白を挿入する (setq Y-axis (cons (make-string width-of-label ? ) Y-axis))) (setq height (1- height))) ;; 基準行を挿入する (setq Y-axis (cons (Y-axis-element 1 width-of-label) Y-axis)) (nreverse Y-axis)))
この関数では、height
の値から始めて1ずつ減らす。
引き算をするたびに、値がY-axis-label-spacing
の倍数かどうかを調べる。
倍数ならば、関数Y-axis-element
を使って数字を書いたラベルを作る。
そうでなければ、関数make-string
を使って空白ラベルを作る。
基準行では、数字1のあとに目盛を付ける。
print-Y-axis
の最終版関数Y-axis-column
が作成したリストは関数print-Y-axis
に渡され、
リストをコラムとして挿入する。
(defun print-Y-axis (height full-Y-label-width &optional vertical-step) "Insert Y axis using HEIGHT and FULL-Y-LABEL-WIDTH. Height must be the maximum height of the graph. Full width is the width of the highest label element. Optionally, print according to VERTICAL-STEP." ;; Value of height and full-Y-label-width ;; are passed by `print-graph'. (let ((start (point))) (insert-rectangle (Y-axis-column height full-Y-label-width vertical-step)) ;; グラフを挿入できるようにポイントを移動 (goto-char start) ;; ポイントを full-Y-label-width だけ進める (forward-char full-Y-label-width)))
print-Y-axis
は、関数insert-rectangle
を使って
関数Y-axis-column
が作成したY軸のラベルを挿入する。
さらに、グラフ本体を書けるようにポイントを正しい位置に移動する。
つぎのようにしてprint-Y-axis
を試せる。
Y-axis-label-spacing Y-axis-tic Y-axis-element Y-axis-columnprint-Y-axis
(print-Y-axis 12 5)
*scratch*
に切り替え、
軸のラベルを書き始めたい場所にカーソルを移動する。
eval-expression
)とタイプする。
yank
)で、graph-body-print
式を
ミニバッファにヤンクする。
Emacsは、先頭が10 -
であるようなラベルを垂直に表示する
(関数print-graph
はheight-of-top-line
の値を渡し、
その値は15になる)。
print-X-axis
X軸のラベルはY軸のラベルに似ているが、目盛は数字の行の上にある。 ラベルはつぎのようである。
| | | | 1 5 10 15
最初の目盛は、グラフの最初のコラムの下にあり、
そのまえには複数の空白文字がある。
これらの空白は、その上にあるY軸のラベルの幅に相当する。
2番目、3番目、4番目、それ以降の目盛はすべて等間隔で、
X-axis-label-spacing
の値で決まる。
X軸の第2行目は、空白をまえに置いた数字で、
変数X-axis-label-spacing
の値で決まる間隔だけ離れている。
グラフ本体の表示に使うシンボルの幅を変更してもラベルの付き方が
変わらないように、変数X-axis-label-spacing
の値は、
symbol-width
を単位とすべきである。
関数print-X-axis
は、関数print-Y-axis
とほぼ同様に作れるが、
X軸は目盛の行と数字の行の2行であることが異なる。
それぞれを表示する別々の関数を書いて、
それらを関数print-X-axis
で組み合わせる。
これは、3段階の処理になる。
print-X-axis-tic-line
を書く。
print-X-axis-numbered-line
を書く。
print-X-axis-tic-line
とprint-X-axis-numbered-line
を使って、
軸の2つの行を描く関数print-X-axis
を書く。
最初の関数は、X軸の目盛を描く。 目盛自体とその間隔を指定する必要がある。
(defvar X-axis-label-spacing (if (boundp 'graph-blank) (* 5 (length graph-blank)) 5) "Number of units from one X axis label to next.")
(graph-blank
の値は、別のdefvar
で設定される。
述語boundp
は、graph-blank
に値が設定されたかどうかを調べる。
設定されていない場合には、boundp
はnil
を返す。
graph-blank
が束縛されていない場合に、この条件式を使わないと、
エラーメッセージSymbol's value as variable is void
を得る。)
(defvar X-axis-tic-symbol "|" "String to insert to point to a column in X axis.")
目標はつぎのような行を作ることである。
| | | |
最初の目盛は、最初のコラムの下にくるように字下げされるが、 これは、Y軸のラベル幅と同じである。
目盛の要素は、目盛からつぎの目盛までのあいだの空白文字と、
目盛のシンボルである。
空白の個数は、目盛のシンボルの幅とX-axis-label-spacing
で決まる。
コードはつぎのようになる。
;;; X-axis-tic-element ... (concat (make-string ;; 空白文字の文字列を作る (- (* symbol-width X-axis-label-spacing) (length X-axis-tic-symbol)) ? ) ;; 空白文字列に目盛のシンボルを繋ぐ X-axis-tic-symbol) ...
つぎは、最初の目盛をグラフの最初のコラムに字下げするために必要な空白文字の
個数を決めることである。
関数print-graph
がfull-Y-label-width
として渡した値を使う。
X-axis-leading-spaces
を計算するコードはつぎのとおりである。
;; X-axis-leading-spaces ... (make-string full-Y-label-width ? ) ...
水平軸の長さを決める必要もある。 この長さは、数のリストの長さと水平軸の目盛の個数で決まる。
;; X-length ... (length numbers-list) ;; tic-width ... (* symbol-width X-axis-label-spacing) ;; number-of-X-tics (if (zerop (% (X-length tic-width))) (/ (X-length tic-width)) (1+ (/ (X-length tic-width))))
以上のことから、X軸の目盛の行を描く関数を書ける。
(defun print-X-axis-tic-line (number-of-X-tics X-axis-leading-spaces X-axis-tic-element) "Print tics for X axis." (insert X-axis-leading-spaces) (insert X-axis-tic-symbol) ; 最初のコラムの真下 ;; 2番目の目盛を正しい位置に挿入する (insert (concat (make-string (- (* symbol-width X-axis-label-spacing) ;; Insert white space up to second tic symbol. (* 2 (length X-axis-tic-symbol))) ? ) X-axis-tic-symbol)) ;; 残りの目盛を挿入する (while (> number-of-X-tics 1) (insert X-axis-tic-element) (setq number-of-X-tics (1- number-of-X-tics))))
数字の行も同じように簡単である。
まず、空白をまえに置いた数字を作る。
(defun X-axis-element (number) "Construct a numbered X axis element." (let ((leading-spaces (- (* symbol-width X-axis-label-spacing) (length (int-to-string number))))) (concat (make-string leading-spaces ? ) (int-to-string number))))
つぎに、最初のコラムの直下に描く「1」から始まる数字の行を書く関数を作る。
(defun print-X-axis-numbered-line (number-of-X-tics X-axis-leading-spaces) "Print line of X-axis numbers" (let ((number X-axis-label-spacing)) (insert X-axis-leading-spaces) (insert "1") (insert (concat (make-string ;; つぎの数字までの空白文字を挿入する (- (* symbol-width X-axis-label-spacing) 2) ? ) (int-to-string number))) ;; 残りの数字を挿入する (setq number (+ number X-axis-label-spacing)) (while (> number-of-X-tics 1) (insert (X-axis-element number)) (setq number (+ number X-axis-label-spacing)) (setq number-of-X-tics (1- number-of-X-tics)))))
最後に、print-X-axis-tic-line
とprint-X-axis-numbered-line
を
使うprint-X-axis
を書く必要がある。
関数では、print-X-axis-tic-line
とprint-X-axis-numbered-line
が
使うローカル変数の値を決定する必要があり、
そのあと、これらの関数を呼び出す。
さらに、2つの行を分ける復帰を書く必要もある。
関数は、5つのローカル変数を指定する変数リストと、 2つの行のおのおのを描く関数の呼び出しから成る。
(defun print-X-axis (numbers-list) "Print X axis labels to length of NUMBERS-LIST." (let* ((leading-spaces (make-string full-Y-label-width ? )) ;; symbol-width は graph-body-print が与える (tic-width (* symbol-width X-axis-label-spacing)) (X-length (length numbers-list)) (X-tic (concat (make-string ;; 空白の文字列を作る (- (* symbol-width X-axis-label-spacing) (length X-axis-tic-symbol)) ? ) ;; 空白を目盛のシンボルに繋ぐ X-axis-tic-symbol)) (tic-number (if (zerop (% X-length tic-width)) (/ X-length tic-width) (1+ (/ X-length tic-width))))) (print-X-axis-tic-line tic-number leading-spaces X-tic) (insert "\n") (print-X-axis-numbered-line tic-number leading-spaces)))
print-X-axis
を試してみよう。
X-axis-tic-symbol
、X-axis-label-spacing
、
print-X-axis-tic-line
とともに
X-axis-element
、print-X-axis-numbered-line
、
print-X-axis
をインストールする。
(progn (let ((full-Y-label-width 5) (symbol-width 1)) (print-X-axis '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16))))
*scratch*
に切り替え、
軸のラベルを描き始める場所にカーソルを置く。
eval-expression
)とタイプする。
yank
)でテスト用の式をミニバッファにヤンクする。
Emacsはつぎのように水平軸を表示する。
| | | | | 1 5 10 15 20
グラフ全体を表示する準備ができた。
正しいラベルを付けたグラフを描く関数は、 まえに作った概略(see Full Graph) とほぼ同じであるが多少追加点もある。
つぎに概略を示す。
(defun print-graph (numbers-list) "説明文..." (let ((height ... ...)) (print-Y-axis height ... ) (graph-body-print numbers-list) (print-X-axis ... )))
最終版は、計画したものと2つの点で異なる。 第一に、変数リストには一度だけ計算する値が追加してある。 第二に、ラベルの行間隔を指定するオプションを持つことである。 後者の機能は本質的であり、これがないと、1画面や1ページに収まらない グラフができてしまう。
この新しい機能のためには、vertical-step
を追加するように
関数Y-axis-column
を変更する必要がある。
関数はつぎのようになる。
;;; 最終版 (defun Y-axis-column (height width-of-label &optional vertical-step) "Construct list of labels for Y axis. HEIGHT is maximum height of graph. WIDTH-OF-LABEL is maximum width of label. VERTICAL-STEP, an option, is a positive integer that specifies how much a Y axis label increments for each line. For example, a step of 5 means that each line is five units of the graph." (let (Y-axis (number-per-line (or vertical-step 1))) (while (> height 1) (if (zerop (% height Y-axis-label-spacing)) ;; ラベルを挿入する (setq Y-axis (cons (Y-axis-element (* height number-per-line) width-of-label) Y-axis)) ;; そうでなければ、空白を挿入する (setq Y-axis (cons (make-string width-of-label ? ) Y-axis))) (setq height (1- height))) ;; 基準行を挿入する (setq Y-axis (cons (Y-axis-element (or vertical-step 1) width-of-label) Y-axis)) (nreverse Y-axis)))
グラフの最大の高さとシンボルの幅は、print-graph
のlet
式で
計算される。
そこで、graph-body-print
がこれらの値を受け取るように変更する必要がある。
;;; 最終版 (defun graph-body-print (numbers-list height symbol-width) "Print a bar graph of the NUMBERS-LIST. The numbers-list consists of the Y-axis values. HEIGHT is maximum height of graph. SYMBOL-WIDTH is number of each column." (let (from-position) (while numbers-list (setq from-position (point)) (insert-rectangle (column-of-graph height (car numbers-list))) (goto-char from-position) (forward-char symbol-width) ;; コラムごとにグラフを描く (sit-for 0) (setq numbers-list (cdr numbers-list))) ;; X軸のラベル用に、ポイントを移動する (forward-line height) (insert "\n")))
最後に、関数print-graph
のコードを示す。
;;; 最終版
(defun print-graph
(numbers-list &optional vertical-step)
"Print labelled bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values.
Optionally, VERTICAL-STEP, a positive integer,
specifies how much a Y axis label increments for
each line. For example, a step of 5 means that
each row is five units."
(let* ((symbol-width (length graph-blank))
;; height
は、最大の数であり
;; 表示幅がもっとも大きくなる
(height (apply 'max numbers-list))
(height-of-top-line
(if (zerop (% height Y-axis-label-spacing))
height
;; そうでなければ
(* (1+ (/ height Y-axis-label-spacing))
Y-axis-label-spacing)))
(vertical-step (or vertical-step 1))
(full-Y-label-width
(length
(concat
(int-to-string
(* height-of-top-line vertical-step))
Y-axis-tic))))
(print-Y-axis
height-of-top-line full-Y-label-width vertical-step)
(graph-body-print
numbers-list height-of-top-line symbol-width)
(print-X-axis numbers-list)))
print-graph
のテスト関数print-graph
を数の短いリストで試してみよう。
Y-axis-column
、graph-body-print
、print-graph
(および、残りのコード)をインストールする。
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1))
*scratch*
に切り替え、
軸のラベルを描き始めたい場所にカーソルを置く。
eval-expression
)とタイプする。
yank
)で、ミニバッファにテスト用の式をヤンクする。
Emacsはつぎのようなグラフを表示する。
10 - * ** * 5 - **** * **** *** * ********* ************ 1 - ************* | | | | 1 5 10 15
一方、vertical-step
の値として2をprint-graph
に渡す
つぎの式を評価すると、
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1) 2)
グラフはつぎのようになる。
20 - * ** * 10 - **** * **** *** * ********* ************ 2 - ************* | | | | 1 5 10 15
(垂直軸の最後が「2」であるのは、バグであろうか機能であろうか? バグと考えるのであれば、かわりに「1」(あるいは、「0」)を表示するように ソースを直せばよい。)
グラフ作成に必要なコードはすべて書いた。 単語やシンボルの個数が10個未満の関数定義はいくつ、 10から19個のものはいくつ、20から29個のものはいくつ、 などなどを示すグラフである。
これは、多段の処理である。 まず、必要なコードをすべてロードしてあることを確認する。
top-of-ranges
の値を変えてしまった場合に備えて、
top-of-ranges
の値を再設定しておこう。
つぎの式を評価する。
(setq top-of-ranges '(10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300)
つぎは、各範囲に属する単語やシンボルの個数のリストを作る。
つぎの式を評価する。
(setq list-for-graph (defuns-per-range (sort (recursive-lengths-list-many-files (directory-files "/usr/local/emacs/lisp" t ".+el$")) '<) top-of-ranges))
筆者のマシンでは、1時間ほど掛かる。
Emacs第19.23版の303個のLispのファイルを調べる。
処理が完了すると、list-for-graph
にはつぎのような値が入る。
(537 1027 955 785 594 483 349 292 224 199 166 120 116 99 90 80 67 48 52 45 41 33 28 26 25 20 12 28 11 13 220)
つまり、筆者の場合、単語やシンボルの個数が10個未満の関数定義は537、 10から19個のものは1027、20から29個のものは955、などなどである。
明らかに、このリストを見るだけで、 ほとんどの関数定義では10から30個の単語やシンボルが含まれことがわかる。
ではグラフを描こう。 しかし、1030行もの高さのグラフを描きたいわけではない。 高さが25行よりも低いグラフを描きたい。 画面や1ページの紙に簡単に収まるような高さのグラフである。
これには、list-for-graph
の各値を1/50に減らす必要がある。
まだ説明していない2つの関数mapcar
とlambda
を使うと、
つぎの小さな関数でできる。
(defun one-fiftieth (full-range) "Return list, each number one-fiftieth of previous." (mapcar '(lambda (arg) (/ arg 50)) full-range))
lambda
式lambda
は、関数名を持たない無名関数を表すシンボルである。
無名関数を使う場合には、その本体を含める必要がある。
つまり、
(lambda (arg) (/ arg 50))
は、「arg
として渡されたものを50で割った商を返す」ような関数定義である。
たとえば、まえに、関数multiply-by-seven
があった。
それは、引数を7倍した。
引数を50で割ることと、名前がないことを除けば、この関数も似ている。
multiply-by-seven
に等価な無名関数は、つぎのようになる。
(lambda (number) (* 7 number))
(See defun。)
3を7倍したければ、つぎのように書ける。
この式は、21を返す。
同様に、つぎのようにも書ける。
100を50で割りたければ、つぎのように書ける。
この式は、2を返す。 関数には100が渡され、50で割られるのである。
lambda
について詳しくは、See Lambda Expressions。
Lispとlambda式は、λ計算(Lambda Calculus)から導かれたのである。
mapcar
mapcar
は、順番に、第2引数の各要素で第1引数を呼び出す。
第2引数は並びであること。
たとえば、
(mapcar '1+ '(2 4 6)) => (3 5 7)
引数に1を加える関数1+
が、リストの各要素に対して実行され、
新たなリストが返される。
これと、第1引数を残りのものに適用するapply
とを対比してほしい
(apply
の説明は、
See Readying a Graph)。
one-fiftieth
の定義では、第1引数はつぎの無名関数である。
(lambda (arg) (/ arg 50))
そして、第2引数はfull-range
であり、
list-for-graph
に束縛される。
式全体はつぎのようになる。
(mapcar '(lambda (arg) (/ arg 50)) full-range))
mapcar
について
詳しくは、See Mapping Functions。
関数one-fiftieth
を使って、
list-for-graph
の各要素を1/50にした要素からなるリストを作れる。
(setq fiftieth-list-for-graph (one-fiftieth list-for-graph))
結果はつぎのようになる。
(10 20 19 15 11 9 6 5 4 3 3 2 2 1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 4)
書くための準備は、これでほとんど整った
(情報の欠落があることに注意してほしい。
上位の範囲の多くは0であるが、それらの範囲にある単語やシンボルの個数の
defun
の数が50より小さいことを意味し、
必ずしもdefun
の数が0ではない)。
「ほとんど整った」といったのは、関数print-graph
にはバグがあるのである。
vertical-step
のオプションはあるが、
horizontal-step
のオプションはない。
top-of-range
は、10間隔で10から300まである。
しかし、関数print-graph
は、1ずつ描く。
これは、もっとも潜在的なバグの典型的なものであり、 無視したことによるバグである。 機能としては存在しないので、コードに書かれておらず、 コードを調べてもこの種のバグは発見できない。 最良の行動は、プログラムを初期の段階で何回もテストすることであり、 コードを可能な限り理解しやすく変更しやすくしておくことである。 たとえすぐにではなくても、最終的には、書いたコードは書き直すことになることを 理解しておいてほしい。 実行するのは難しい格言である。
関数print-X-axis-numbered-line
を直す必要がある。
そして、関数print-X-axis
とprint-graph
も、
対応するように直す必要がある。
多くを直す必要はない。
ちょっとしたことが1つである。
目盛に数字を合わせるだけである。
これには少々考える必要がある。
つぎに修正したprint-X-axis-numbered-line
を示す。
(defun print-X-axis-numbered-line (number-of-X-tics X-axis-leading-spaces &optional horizontal-step) "Print line of X-axis numbers" (let ((number X-axis-label-spacing) (horizontal-step (or horizontal-step 1))) (insert X-axis-leading-spaces) ;; まえにある余分な空白を削除する (delete-char (- (1- (length (int-to-string horizontal-step))))) (insert (concat (make-string ;; 空白を挿入する (- (* symbol-width X-axis-label-spacing) (1- (length (int-to-string horizontal-step))) 2) ? ) (int-to-string (* number horizontal-step)))) ;; 残りの数を挿入する (setq number (+ number X-axis-label-spacing)) (while (> number-of-X-tics 1) (insert (X-axis-element (* number horizontal-step))) (setq number (+ number X-axis-label-spacing)) (setq number-of-X-tics (1- number-of-X-tics)))))
Infoで読んでいる場合には、
新しい版のprint-X-axis
やprint-graph
もあるので、これらを評価する。
印刷物で読んでいる場合には、変更した行をつぎに示してある
(コードを全体は多すぎる)。
インストールしたら、コマンドprint-graph
をつぎのようにして呼ぶ。
(print-graph fiftieth-list-for-graph 50 10)
グラフはつぎのとおりである。
1000 - * ** ** ** ** 750 - *** *** *** *** **** 500 - ***** ****** ****** ****** ******* 250 - ******** ********* * *********** * ************* * 50 - ***************** * * | | | | | | | | 10 50 100 150 200 250 300 350
関数のもっとも大きなグループは、10から19個の単語やシンボルを含むものである。
%
(剰余関数)
: Compute a Remainder
(debug)
(コード内): debug-on-quit
*
(乗算)
: defun
*
読み出し専用バッファ用
: read-only buffer
*Backtrace*
バッファ: debug
*scratch*
バッファ: print-elements-of-list
.emacs
ファイル: Beginning a .emacs File, Emacs Initialization
.emacs
ファイル(簡単な拡張): Simple Extension
.emacs
ファイルのカスタマイズ: Emacs Initialization
/
(除算)
: large-case
<=
(小さいか等しい)
: Inc Example parts
> (より大きい)
: if
add-hook
: Text and Auto-fill
and
: fwd-para let
append-to-buffer
: append-to-buffer
apply
: Columns of a graph
apropos
: Columns of a graph
autoload
: Autoload
beginning-of-buffer
: beginning-of-buffer
buffer-file-name
: Buffer Names
buffer-menu,
バインド
: Keybindings
buffer-name
: Buffer Names
cancel-debug-on-entry
: debug-on-entry
car,
紹介
: car cdr & cons
cdr,
紹介
: car cdr & cons
compare-windows
: Keybindings
concat
: Data types
cond
: Recursion with cond
cons,
紹介
: cons
copy-region-as-kill
: copy-region-as-kill
copy-to-buffer
: copy-to-buffer
count-words-in-defun
: count-words-in-defun
count-words-region
: count-words-region
current-buffer
: Getting Buffers
debug
: debug
debug-on-entry
: debug-on-entry
debug-on-error
: debug
debug-on-quit
: debug-on-quit
default-mode-line-format
: Mode Line
default.el
初期化ファイル: Site-wide Init
defun
: defun
defun
スペシャルフォーム: defun
defun
内の単語の数え上げ: count-words-in-defun, Words in a defun
defvar
: defvar
delete-region
: delete-region
describe-function
: simplified-beginning-of-buffer
describe-function,
紹介
: Finding More
directory-files
: Files List
edebug
: edebug
edit-options
: edit-options
edit-options,
紹介
: defvar
eobp
: fwd-para between paragraphs
eq
: Review
eq
(使用例)
: copy-region-as-kill body
equal
: Review
error
: rotate-yk-ptr body
etags
: etags
find-tags
: Finding More
forward-paragraph
: forward-paragraph
forward-sentence
: forward-sentence
global-set-key
: Keybindings
global-unset-key
: Keybindings
graph-body-print
: graph-body-print
graph-body-print
最終版
: Print Whole Graph
if
: if
if
による判定: if
indent-tabs-mode
: Indent Tabs Mode
insert-buffer
: insert-buffer
insert-buffer-substring
: append-to-buffer
int-to-string
: Y Axis Element
interactive
: Interactive
interactive,
使用例
: insert interactive expression
interactive
のオプション: Interactive Options
kill-append
: kill-append function
kill-region
: kill-region
lambda
: lambda
length
: length
lengths-list-file
: lengths-list-file
lengths-list-many-files
: Several files
let
: let
let
式の構造: Parts of let Expression
let
式の非初期化変数: Uninitialized let Variables
let
式の例: Sample let Expression
line-to-top-of-window
: Simple Extension
list-buffers,
リバインド
: Keybindings
list-options
: edit-options
load-library
: Loading Files
load-path
: Loading Files
looking-at
: fwd-para between paragraphs
make-string
: Y Axis Element
mapcar
: mapcar
mark
: save-excursion
mark-whole-buffer
: mark-whole-buffer
match-beginning
: fwd-para no fill prefix
max
: Columns of a graph
message
: message
min
: Columns of a graph
nil
: Truth & Falsehood
nil
、単語の歴史: Buffer Names
nreverse
: Files List
nthcdr
: copy-region-as-kill, nthcdr
occur
: Keybindings
optional
: Optional Arguments
or
: insert or
other-buffer
: Getting Buffers
point
: save-excursion
print-elements-of-list
: print-elements-of-list
print-elements-recursively
: Recursion with list
print-graph
最終版
: Print Whole Graph
print-graph
の変数リスト: print-graph Varlist
print-X-axis
: X Axis Tic Marks
print-X-axis-numbered-line
: X Axis Tic Marks
print-X-axis-tic-line
: X Axis Tic Marks
print-Y-axis
: print-Y-axis Final
prog1
: fwd-para between paragraphs
progn
: progn
re-search-forward
: re-search-forward
recursive-count-words
: recursive-count-words
recursive-graph-body-print
: recursive-graph-body-print
recursive-lengths-list-many-files
: Several files recursively
regexp-quote
: fwd-para let
reverse
: Files List
rotate-yank-pointer
: rotate-yank-pointer, Yanking
save-excursion
: save-excursion
save-restriction
: save-restriction
search-forward
: search-forward
sentence-end
: sentence-end
set
: Using set
set-buffer
: Switching Buffers
setcar
: setcar
setcdr
: setcdr
setq
: Using setq
simplified-beginning-of-buffer
: simplified-beginning-of-buffer
site-init.el
初期化ファイル: Site-wide Init
site-load.el
初期化ファイル: Site-wide Init
sort
: Sorting
switch-to-buffer
: Switching Buffers
TAGS
ファイルの作成: etags
the-the
: the-the
top-of-ranges
: Files List
triangle-bugged
: debug
triangle-recursively
: Recursive triangle function
what-line
: what-line
while
: while
X-axis-element
: X Axis Tic Marks
Y-axis-column
: Y-axis-column
Y-axis-column
最終版
: Print Whole Graph
Y-axis-label-spacing
: Compute a Remainder
Y-axis-tic
: Y Axis Element
yank
: yank, Yanking
yank-pop
: yank-pop
zap-to-char
: zap-to-char
zerop
: rotate-yk-ptr body
.emacs
ファイル): Emacs Initialization
コマンド(command)
(定義): How to Evaluate
*Backtrace*
バッファ): debug
ナロイング(narrowing)
(定義): Buffer Size & Locations
フォーム(form)
(定義): Lisp Atoms
ポイント(point)
(定義): Buffer Size & Locations
ローカル変数(local variable)
(定義): let
引数(argument)
(定義): Arguments
引数リスト(argument list)
(定義): defun
関数(function)
(定義): Making Errors
関数定義(function definition)
(定義): defun
空リスト
(定義): Lisp Atoms
呼び出し(call)
(定義): Switching Buffers
式(expression)
(定義): Lisp Atoms
default.el
): Site-wide Init
site-init.el
): Site-wide Init
%
: Compute a Remainder
真の場合の動作(then-part)
(定義): if
束縛(bind)
(定義): set & setq
対話的関数(interactive function)
(定義): How to Evaluate
defun
): Words in a defun
判定条件(if-part)
(定義): if
評価(evaluate)
(定義): Run a Program
副作用(side effect)
(定義): Evaluation
文字列(string)
(定義): Lisp Atoms
変数リスト(varlist)
(定義): Parts of let Expression
本体(body)
(定義): defun
Robert J. Chassellは、1985年以来GNU Emacsの仕事をしている。 執筆や編集、EmacsとEmacs Lispを教えており、 Free Software Foundation, Inc.の理事、幹事/会計でもある。 社会史や経済史に興味を持っており、自家用飛行機を操縦する。
car
、cdr
、cons
defun
内の単語の数え上げ
.emacs
the-the
car
、cdr
、cons
defun
内の単語の数え上げ
.emacs
the-the
ISBN4-88735-020-1、株式会社プレンティスホール出版
単語「argument」の異なる2つの意味、つまり、数学での意味と
日常語での意味がどのように成立したかを調べると興味深い。
Oxford English Dictionaryによれば、to make clear, prove
(明らかにする、証明する)
を意味するラテン語が語源である。
これから、「証明として与えられた証拠」という一方の意味が派生して、
「与えられた情報」を意味するようになり、Lispでの意味になるのである。
もう一方の意味としては、「ある仮定とは反対の仮定をする」を意味するようになり、
議論するという意味になったのである
(英語では異なる2つの意味が同時に1つの単語に与えられている。
対照的に、Emacs Lispでは、異なる2つの関数定義を同時にシンボルに与えることは
できない)。
ドットペアーを作成するためにアトムと要素をcons
することも
できる。
ここでは、ドットペアーについては
説明しないので、Dotted Pair Notationを参照。