Node:Top, Next:, Previous:(dir), Up:(dir)


Node:PrePreface, Next:, Previous:Top, Up:Top

訳者まえがき

本書は、「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」に固有の設定 ...)
  画面端末の場合の設定 ...)


Node:Preface, Next:, Previous:PrePreface, Up:Top

はじめに

テキストエディタGNU Emacsの大部分はEmacs Lispと呼ばれるプログラミング 言語で書かれている。 このプログラミング言語で書いたコードは、ユーザーが指令を与えた ときに何をすべきかをコンピュータに指示するソフトウェア(一連の命令)である。 Emacsは、Emacs Lispで新たにコードを書いてエディタの拡張機能として 簡単に追加できるように設計されている。 「拡張可能なエディタ」と呼ばれる結縁である。

(Emacsは編集機能以上のものを提供するため、「拡張可能な計算環境」とでも 呼ぶべきであるが、少々いいづらい。 マヤ暦や月の満ち欠けを調べたり、多項式を簡約化したり、コードをデバッグしたり、 ファイルを管理したり、手紙を読んだり、本を書いたりなどの Emacsで行えるすべてのことは、もっとも一般的な意味で編集である。)

Emacs Lispはテキストエディタに関連付けて考えられがちであるが、 それ自体で1つのプログラミング言語である。 他のプログラミング言語と同様に使える。

プログラミングを理解したい、Emacsを拡張したい、プログラマになりたいという 読者もいることであろう。 Emacs Lispの入門である本書は、プログラミングの基本を学ぶための指針を与え、 さらに重要なことは、自力で学習する方法を示すために執筆したものである。


Node:On Reading this Text, Next:, Previous:Preface, Up:Preface

本書には、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を利用できるのである。 この独立性は好ましいだけでなく利点でもある。


Node:Who You Are, Next:, Previous:On Reading this Text, Up:Preface

対象とする読者

本書は、プログラマではない人向けの初歩の入門書である。 すでにプログラマである読者には、本書は物足りないであろう。 というのは、そのような読者はすでにリファレンスマニュアルを存分に読めるように なっており、本書の構成は間延びして見えるであろう。

経験あるプログラマは、本書をつぎのように評価してくれた。

リファレンスマニュアルで学ぶほうが好きである。 各段落に「飛び込んで」、段落のあいだで「息つぎ」する。

段落を読み終えたときには、そこで取り上げた話題は完結しており、 必要なことは(以降の段落でより詳しく説明する場合を除いて) すべてわかったと仮定したい。 よく構成されたリファレンスマニュアルには、 冗長な部分がなく、必要な情報への索引が整備されているはずである。

本書は、このような人向けではない!

第一に、おのおののことがらを少なくとも3回は説明するように努めた。 1回目は紹介、2回目は使い方、3回目は別の使い方や復習である。

第二に、1つの話題に関するすべての情報を1か所にまとめることはせずに、 1つの段落に詰め過ぎないようにした。 筆者の考え方では、そうしないと読者に重荷を背負わせることになるからである。 かわりに、その時点で必要なことのみを説明するように努めた (あとで詳しく説明する場合に備えて、少々余分に説明する場面もある)。

1回読むだけで、すべてを理解してもらえるとは考えていない。 読者は、説明内容を「わかったつもり」にしておく必要があるだろう。 重要なことがらを正しく読者に指し示し、 注意を促すように本書を構成したつもりである。

いくつかの段落には、「飛び込んで」もらうしかなく、 それ以外に読み進む方法はない。 しかし、そのような段落の個数は少なくするように努めた。 本書は登頂困難な山ではなく、楽に歩ける小山である。

本書Emacs Lispプログラミング入門には、 姉妹編と呼ぶべきドキュメント がある。 リファレンスマニュアルには本書より詳しい説明がある。 リファレンスマニュアルでは、1つの話題に関する情報は1か所にすべてまとめてある。 上で述べたようなプログラマは、そちらを参照すべきである。 もちろん、本書を読み終えて自分のプログラムを書くときには、 リファレンスマニュアルが有用であるはずである。


Node:Lisp History, Next:, Previous:Who You Are, Up:Preface

Lispの歴史

Lispは、人工知能の研究のために、 1950年代末にマサチューセッツ工科大学で初めて開発された。 Lisp言語はその強力な機能のため、 エディタコマンドを書くなどの他の目的にも優れている。

GNU Emacs Lispは、1960年代にMITで開発されたMaclispから多くを受け継いでいる。 1980年代に標準規格となったCommon Lispからも一部を受け継いでいる。 しかし、Emacs Lispは、Common Lispよりもずっと単純である (Emacsの標準ディストリビューションには、 Common Lispの多くの機能をEmacs Lispに追加するための 機能拡張用ファイルcl.elがある)。


Node:Note for Novices, Next:, Previous:Lisp History, Up:Preface

初心者へ一言

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のみを意味する。


Node:Thank You, Previous:Note for Novices, Up:Preface

謝 辞

本書の執筆に協力していただいた方々に感謝したい。 特に、Jim Blandy、Noah Friedman、Jim Kingdon、Roland McGrath、 Frank Ritter、Randy Smith、Richard M. Stallman、 Melissa Weisshausに感謝したい。 辛抱強く励ましてくれたPhilip JohnsonとDavid Stampeにも感謝したい。 誤りがあれば筆者の責任である。


Node:List Processing, Next:, Previous:Preface, Up:Top

リスト処理

慣れていない人の目には、Lispは不可思議なプログラミング言語である。 Lispのコードには、いたるところに括弧がある。 「Lots of Isolated Silly Parentheses(奇妙な括弧が多い)」の略であると 批判する人達もいる。 しかし、この批判は不当である。 LispはLISt Processingの略であり、括弧で囲んだリスト(list) (および、リストのリスト)を扱うプログラミング言語である。 括弧はリストの境界を表す。 リストの直前にアポストロフィ、つまり、引用符'を付ける場合もある。 リストはLispの基本である。


Node:Lisp Lists, Next:, Previous:List Processing, Up:List Processing

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

このリストの要素は、thislisthasの単語と、 リスト(a list inside of it)である。 内側のリストは、alistinsideofitの単語からできている。


Node:Lisp Atoms, Next:, Previous:Lisp Lists, Up:Lisp Lists

Lispのアトム

Lispでは、これまで単語と呼んできたものをアトム(atoms)と呼ぶ。 この用語はアトム(原子)の歴史的な意味からきており、 「不可分」ということである。 Lispに関していえば、リストに用いてきた単語はそれ以上には小さく分割できず、 プログラムの一部として同じものを意味する。 数や+のような1文字の記号についてもそうである。 一方、アトムとは異なり、リストは部分に分割できる (See car cdr & cons)。

リストでは、アトムを空白で区切る。 アトムは、括弧のすぐ隣にあってもよい。

技術的には、Lispのリストは、空白で区切ったアトムを囲む括弧、 リストを囲む括弧、アトムやリストを囲む括弧から成る。 リストは、たった1個のアトムを含むだけでも、まったく含まなくてもよい。 何も含まないリストは()のように書き、空リスト(empty list)と呼ぶ。 空リストは、それ以外のものとは異なり、アトムであると同時にリストでもある。

アトムやリストを表示したものをシンボリック式(symbolic expressions)、 あるいは、より簡素にはS式(s-expressions)と呼ぶ。 用語「式(expression)」そのものでは、表示したアトムやリスト、あるいは、 コンピュータ内部に格納したアトムやリストを意味する。 しばしば、これらを区別せずに用語「式(expression)」を使う (さらに、多くの書籍では式の同義語として用語「フォーム(form)」を使う)。

われわれの宇宙を構成するアトム(原子)は、 それらが不可分であると考えられた時代に命名されたものであるが、 物質原子は不可分ではないことが知られている。 原子の一部を分割したり、ほぼ同じ大きさに分裂させたりできる。 物質原子は、その真の性質が発見される以前に命名されたのである。 Lispでは、配列などのある種のアトムは構成部分に分割できるが、 この分割機構はリストを分割する機構とは異なる。 リスト操作に関する限り、リストのアトムは分割できない。

英語の場合と同様に、Lispのアトムを構成する文字は、 単語を作り上げる個々の文字とは異なる。 たとえば、南米のナマケモノを表す単語ai(ミツユビナマケモノ)は、 2つの単語aiとはまったく異なる。

自然界には多種類の原子が存在するが、Lispには数種類のアトムしかない。 たとえば、37、511、1729などの数(numbers)+fooforward-lineなどの シンボル(symbols)である。 これまで例にあげた単語はすべてシンボルである。 Lispの日常の習慣では、用語「アトム」をあまり使わない。 というのは、プログラマは扱っているアトムの種類を特定しようとするからである。 Lispのプログラミングでは、リスト内のシンボル(やときには数)を扱う (括弧書き「(やときには数)」の原文(and sometimes numbers)は、 アトムを空白で区切って括弧で囲んであり、 しかも、Lispの句読点記号以外は含まないのでLispのリストである)。

さらに、二重引用符で囲まれたテキストは(文であろうと段落であろうと) アトムである。 つぎに例を示す。

'(this list includes "text between quotation marks.")

Lispでは、句読点記号や空白を含みこのように囲まれたテキストは単一のアトムである。 この種のアトムは文字列(string)と呼ばれ、 コンピュータが人間向けに出力するメッセージに使う。 文字列は、数やシンボルとは異なる別の種類のアトムであり、使い方も異なる。


Node:Whitespace in Lists, Next:, Previous:Lisp Atoms, Up:Lisp Lists

リスト内の空白

リスト内の空白の個数はいくつでもよい。 Lisp言語の視点からすれば、

'(this list
   looks like this)

は、つぎとまったく同等である。

'(this list looks like this)

どちらの例もLispにとっては同じリストであり、 thislistlookslikethisのシンボルからこの順に構成されたリストである。

余分な空白や改行は人間がリストを見やすくするためのものである。 Lispが式を読み取るとき、余分な空白をすべて取り除く (ただし、アトムとアトムのあいだには、 これらを区切るために少なくとも1つの空白が必要である)。

奇妙に思えるかもしれないが、これまでの例で、 Lispのすべてのリストがどのようなものであるかを見てきた。 Lispのリストは多かれ少なかれこれまでの例のようなものであり、 もっと長かったり複雑なだけである。 要約すれば、リストは括弧で囲まれたものであり、 文字列は二重引用符で囲まれたものであり、シンボルは単語のようなものであり、 数は数字列である (鈎括弧やドットや特別な数種の文字を使う場合もあるが、 しばらくはこれらを使わない)。


Node:Typing Lists, Previous:Whitespace in Lists, Up:Lisp Lists

GNU Emacsのリスト入力補佐機能

GNU EmacsのLisp InteractionモードやEmacs LispモードでLispの式を 入力する場合には、Lispの式を読みやすく整形するためのコマンドを利用できる。 たとえば、<TAB>キーを押すと、カーソルを置いた行を自動的に字下げする。 あるリージョンのコードを正しく字下げするコマンドは、 慣習的にM-C-\にバインドされている。 リストの各要素が、どのリストに属するかがわかりやすくなるように字下げする。 つまり、内側のリストの各要素は、 そのリストを囲むリストの要素よりも字下げされる。

さらに、閉じ括弧をタイプするとEmacsは対応する開き括弧に一時的に カーソルを移動して、対応関係がわかるようにする。 Lispに与える個々のリストは括弧の対応が取れている必要があるので、 これはとても便利である (Emacsのモードに関して詳しくは、 See Major Modes)。


Node:Run a Program, Next:, Previous:Lisp Lists, Up:List Processing

プログラムの実行

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インタープリタがどうするかを説明しよう。


Node:Making Errors, Next:, Previous:Run a Program, Up:List Processing

エラーメッセージの生成

偶然引き起こした場合は気にしないでよいが、ここでは エラーメッセージを生成するようなコマンドを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を加算できるので、 シンボル+にはコンピュータが実行すべき命令列が与えられており、 しかも、それらは+に続く数を加算する命令であると推理できる。


Node:Names & Definitions, Next:, Previous:Making Errors, Up:List Processing

シンボル名と関数定義

これまでに説明してきたことをもとに、Lispの別の特徴を明確にすることができる。 +のようなシンボルは、それ自身はコンピュータが実行すべき命令列 ではないという重要な特徴である。 そのかわりに、定義、すなわち、命令列を探すために シンボルを(一時的に)使うのである。 われわれが見ているものは、命令列を探すための名前である。 人の名前も同じように使われる。 筆者はBobと呼ばれているが、私はBobの3文字 ではなく、意識のある生命体である。 名前そのものは私ではなく、私を指すために名前を使うのである。

Lispでは、1つの命令列に複数の名前を結び付けることができる。 たとえば、コンピュータの加算命令列をシンボルplusにも+にも 結び付けられる(そのようなLispの方言もある)。 人間の世界でも、筆者をBobと呼んだりRobertと呼んだりでき、 それ以外の単語でもよい。

その一方で、シンボルには1つの関数定義しか結び付けられない。 さもなければ、どちらの定義を採用すべきかコンピュータが混乱する。 これを人間の世界に当てはめると、 Bobという名前を持つ人は世界中で1人に限られることになる。 しかし、名前が参照する関数定義を変更するのは容易である (See Install)。

Emacs Lispは巨大なので、関数が属するEmacsの構成部分がわかるように シンボルを命名する慣習がある。 したがって、Texinfoを扱うすべての関数の名前はtexinfo-で始まり、 メールを読むことに関連する関数の名前はrmail-で始まる。


Node:Lisp Interpreter, Next:, Previous:Names & Definitions, Up:List Processing

Lispインタープリタ

これまでの説明をもとに、リストの評価をLispインタープリタに命じると Lispインタープリタが何をするかを理解することができる。 まず、リストの直前にクオートがあるかどうかを調べる。 クオートがあれば、リストを返すだけである。 一方、クオートがなければ、インタープリタはリストの先頭要素を調べ、 それに関数定義があるかどうかを調べる。 関数定義があれば、インタープリタはその関数定義内の命令列を実行する。 さもなければ、インタープリタはエラーメッセージを出力する。

これがLispの動作であり、単純である。 すぐに説明するが、これに加えて複雑なことがらもあるが、以上が基本である。 当然、Lispプログラムを書くには、関数定義の書き方、名前への結び付け方、 および、これらを読者やコンピュータに混乱のないように行う方法を知る必要がある。

では、複雑なことがらの最初のことを説明しよう。 Lispインタープリタは、リストに加えて、 クオートもせず括弧で囲まれてもいないシンボルを評価できる。 この場合、Lispインタープリタは、 変数(variable)としてのシンボルの値を決定しようとする。 これについては変数に関する節で説明する (See Variables)。

複雑なことがらの2番目は、特殊な関数があり、 これらは普通の方式のように動作しないことである。 これらをスペシャルフォーム(special forms)と呼ぶ。 関数の定義などの特殊なことを行うものであり、それらの個数は多くはない。 以下のいくつかの章では、重要なスペシャルフォームのいくつかを紹介する。

3番目で最後の複雑なことがらはつぎのとおりである。 Lispインタープリタが探しあてた関数がスペシャルフォームでなく、 しかも、それがリストの一部である場合には、 Lispインタープリタはリストの内側にリストがあるかどうかを調べる。 内側にリストがあれば、Lispインタープリタは内側のリストを処理してから、 外側のリストを処理する。 内側のリストの内側にもリストが含まれている場合には、 それを処理してから内側のリストを処理する。 つねに、もっとも内側のリストを最初に処理する。 インタープリタはもっとも内側のリストの結果を得るためにそれを処理する。 その結果はそれを含む式で使われる。

そうでない場合には、インタープリタは左から右への順で式を1つずつ処理する。


Node:Byte Compiling, Previous:Lisp Interpreter, Up:Lisp Interpreter

バイトコンパイル

インタープリタには別の側面もある。 Lispインタープリタは2種類のものを解釈できる。 本書で取り上げている人が読める形式のコードと、 特別な処理を施し人には読めない形式の バイトコンパイル(byte compiled)コードである。 バイトコンパイルしたコードは、人間向けのコードに比べて実行が速い。

byte-compile-fileのようなコンパイルコマンドを実行すれば、 人間向けのコードをバイトコンパイルコードに変換できる。 バイトコンパイルコードは、拡張子.elではなく拡張子.elcで 終わるファイルに収容するのが一般的である。 ディレクトリemacs/lispには両方の種類のファイルがあるが、 読むのは拡張子.elのファイルである。

実用上は、Emacsを調整したり拡張したりするのがほとんどであろうから、 バイトコンパイルする必要はなく、ここではこれ以上取り上げない。 バイトコンパイルについて詳しくはSee Byte Compilation


Node:Evaluation, Next:, Previous:Lisp Interpreter, Up:List Processing

評 価

Lispインタープリタが式を処理することを評価する(evaluation)と呼ぶ。 インタープリタが「式を評価する」という。 これまでにも、この用語を何度か使ってきた。 この用語は日常の言葉使いからきている。 つまり、Webster's New Collegiate Dictionaryによれば、 「価値や量を見定める、見積もること」である。

式を評価し終えると、Lispインタープリタは、関数定義で与えられた命令列を コンピュータが実行した結果を返す(return)のが一般的であるが、 関数の処理を諦めてエラーメッセージを生成する場合もある (インタープリタ自体を別の関数に渡したり、 「無限ループ」と呼ばれる無制限に同じことを繰り返すこともある。 これらの動作は一般的ではないので、ここでは無視することにする)。 ほとんどの場合、インタープリタは値を返す。

インタープリタは、値を返すと同時に、 カーソルを移動したりファイルをコピーしたりなどの別の動作も行う。 この種の別の動作は、副作用(side effect)と呼ばれる。 結果の表示などの人間が重要と考える動作は、 Lispインタープリタによっては「副作用」であることが多い。 この用語は奇妙に聞こえるかもしれないが、 副作用の使い方を学ぶのはかなり簡単である。

まとめると、Lispインタープリタは、 シンボリック式を評価するとほとんどの場合は値を返すが、副作用を伴うこともある。 あるいは、エラーを生成する。


Node:Evaluating Inner Lists, Previous:Evaluation, Up:Evaluation

内側のリストの評価

内側にリストを含んだリストを評価するときには、 内側のリストを評価して得られた値を、 外側のリストを評価するときの情報として使う場合がある。 そのために、内側の式を最初に評価するのである。 それが返した値を外側の式で使用する。

このような評価の過程を、加算の例題で調べることにしよう。 つぎの式の直後にカーソルを置いて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では、数を評価するとその数そのものを得るのである。 これが数とシンボルの違いである。 +のようなシンボルで始まるリストを評価すると、 +に結び付けた関数定義の命令列を実行した結果の値を得る。 つぎの節で説明するように、シンボルそのものを評価すると別のことが起こる。


Node:Variables, Next:, Previous:Evaluation, Up:List Processing

変 数

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


Node:Void Variable, Previous:Variables, Up:Variables

値のないシンボルに対するエラーメッセージ

値が束縛されていないシンボルを評価すると、エラーメッセージを得る。 たとえば、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)である旨のエラーメッセージが 報告されたのである。


Node:Arguments, Next:, Previous:Variables, Up:List Processing

引 数

どのように関数に情報が伝えられるかを見るために、 お馴染みの2足す2の例を使ってみよう。 Lispでは、つぎのように書く。

(+ 2 2)

この式を評価すると、エコー領域には数4が表示される。 Lispインタープリタが行ったことは、+のあとに続く数の加算である。

+が加算した数のことを、関数+引数(arguments)と呼ぶ。 これらの数は、関数に与えられた、つまり、渡された情報である。

「argument(引数)」という用語は数学での用法からきており、 2人のあいだの議論のことではない。 ここでは、+という関数へ与えられた情報を意味する。 Lispでは、関数に対する引数は、その関数のあとに続くアトムやリストである。 これらのアトムやリストを評価して返された値が関数へ渡される。 関数が異なれば、必要な引数の個数も異なり、 引数をまったく必要としない関数もある。 2


Node:Data types, Next:, Previous:Arguments, Up:Arguments

引数のデータ型

関数に渡すデータの型は、関数が使用する情報の種類に応じて決まる。 +は数を加算するので、 +のような関数への引数は数値である必要がある。 別の関数では別の種類のデータの引数が必要である。

たとえば、関数concatは複数の文字列を繋いで1つの文字列を生成する。 したがって、引数は文字列である。 文字列abcdefを繋ぐ(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は、文字列である引数から部分文字列を切り出すことが できるだけであり、数やシンボルなどのそれ以外の種類のアトムは扱えない。


Node:Args as Variable or List, Next:, Previous:Data types, Up:Arguments

引数としての変数やリストの値

引数は、評価したときに値を返すシンボルでもよい。 たとえば、シンボル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の直前には空白が必要である)。


Node:Variable Number of Arguments, Next:, Previous:Args as Variable or List, Up:Arguments

可変個数の引数

concat+*などのある種の関数は、任意個数の引数を取る (*は乗算のシンボルである)。 これは、以下の式のおのおのを通常の方法で評価するとわかる。 =>のあとのテキストがエコー領域に表示され、 =>は「の評価結果は」と読めばよい。

まず、関数に引数がない場合にはつぎのとおりである。

(+)       => 0

(*)       => 1

つぎは、関数に引数が1つの場合である。

(+ 3)     => 3

(* 3)     => 3

今度は、関数に引数が3つある場合である。

(+ 3 4 5) => 12

(* 3 4 5) => 60


Node:Wrong Type of Argument, Next:, Previous:Variable Number of Arguments, Up:Arguments

引数に誤った型のオブジェクトを指定

誤った型の引数を関数へ渡すと、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-ppは、 Lispプログラミングの早い時期に始まった慣習である。 pは「述語(predicate)」の略である。 初期のLisp研究者が用いた専門用語では、 述語とはある性質が真か偽か調べる関数を意味する。 したがって、pがあることで、integer-or-marker-pは、 与えられた引数が整数であるかマーカーであるかの真偽を調べる関数の名前である ことがわかる。 pで終わるそのほかのLispシンボルには、 引数の値が0であるかを調べる関数zerop、 引数がリストであるかを調べる関数listpがある。

エラーメッセージの最後の部分はシンボルhelloである。 これは+に渡された引数の値である。 正しい型のオブジェクトが加算に渡されれば、 渡された値はhelloのようなシンボルではなく37のような数であるはずである。 そうであったならば、エラーメッセージが表示されることはなかったのである。


Node:message, Previous:Wrong Type of Argument, Up:Arguments

関数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."と表示される。


Node:set & setq, Next:, Previous:Arguments, Up:List Processing

変数への値の設定

変数に値を与える方法はいくつかある。 1つの方法は関数setか関数setqを使うことである。 別の方法はlet(see let)を使うことである (この過程を専門用語では変数を値に束縛(bind)するという)。

つぎの節ではsetsetqの動作を説明するとともに、 引数がどのように渡されるかも説明する。


Node:Using set, Next:, Previous:set & setq, Up:set & setq

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はその返された値に値を設定しようとする。 このように関数を動作させたい場面もあるが、そのような場面は少ない)。


Node:Using setq, Next:, Previous:Using set, Up:set & setq

setqの使い方

実用上、setの第1引数をほとんどつねにクオートするはずである。 setで第1引数をクオートする組み合わせは多用されるので、 スペシャルフォームsetqが用意してある。 このスペシャルフォームはsetとほとんど同じであるが、 第1引数を自動的にクオートするので、引用符をタイプする必要はない。 さらに、便利なように、setqでは、複数の異なる変数に異なる値を 1つの式で設定することもできる。

setqを用いて、変数carnivoresの値を リスト'(lion tiger leopard)とするには、つぎの式を使う。

(setq carnivores '(lion tiger leopard))

これはsetを用いた場合とほぼ同じであるが、 setqでは第1引数を自動的にクオートするのが異なる (setqqは、クオート(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行に書いてもよいが、ページの幅に収まらない。 人間には、適切にフォーマットしたリストは読みやすいものである。)

「代入」という用語を用いたが、setsetqの動作を考える 別の方法もある。 つまり、setsetqは、シンボルがリストを指す(point) ようにするのである。 この考え方は非常によく使われ、うしろの章では、名前の一部に「pointer」がある シンボルを見ることになる。 この名称は、シンボルには特にリストなどの値が結び付けらている、 あるいは、シンボルがリストを「指す」ように設定されていることに由来する。


Node:Counting, Previous:Using setq, Up:set & setq

数え上げ

ここではカウンタで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の値が変更されるのである。


Node:Summary, Next:, Previous:set & setq, Up:List Processing

まとめ

Lispを学ぶことは、登り始めがもっとも険しい小山を登るようなものである。 読者はもっとも困難な部分を登り終えたのであり、 あとは、先へ進むほど楽になる。

本章をまとめるとつぎのようになる。


Node:Error Message Exercises, Previous:Summary, Up:List Processing

演習問題

簡単な演習問題をあげておく。


Node:Practicing Evaluation, Next:, Previous:List Processing, Up:Top

評価の練習

Emacs Lispにおける関数定義の書き方を学ぶまえに、これまでに説明してきた さまざまな式の評価に時間を割いてみるのも有益であろう。 リストの先頭(あるいは唯一)の要素が関数であるようなリストである。 バッファに関する関数は、単純でしかも興味深いので、これらから始めよう。 本節では、それらのいくつかを評価してみる。 他の節では、バッファに関連した数個の別の関数のコードを調べて、 それらがどのように書かれているかを見てみる。


Node:How to Evaluate, Next:, Previous:Practicing Evaluation, Up:Practicing Evaluation

Emacs Lispに、カーソルの移動や画面上のスクロールなどの 編集コマンドを与えるたびに、先頭要素が関数である式を評価しているEmacsはこのようにして動いている。

キーをタイプすると、Lispインタープリタに式を評価させることになり、 そのようにして操作しているのである。 テキストをタイプした場合でさえも、Emacs Lispの関数を評価しており、 タイプした文字を単に挿入する関数self-insert-commandを使った 関数を評価しているのである。 キーをタイプすることで評価される関数は、 対話的(interactive)関数とかコマンド(commands)と呼ばれる。 関数を対話的にする方法については、関数定義の書き方の章で例を示す。 See Interactive

キーボードコマンドをタイプする以外にも、 式を評価する方法についてはすでに説明した。 すなわち、リストの直後にカーソルを置いてC-x C-eとタイプするのである。 本節の残りでは、この方法を用いる。 これ以外にも式を評価する方法があり、必要に応じて他の節で説明する。

これからの数節で説明する関数は、評価を練習すること以外にも、 それ自体、重要なものである。 これらの関数を学ぶことで、バッファとファイルの違い、 バッファを切り替える方法、バッファ内での位置を調べる方法が明らかになる。


Node:Buffer Names, Next:, Previous:How to Evaluate, Up:Practicing Evaluation

バッファ名

2つの関数、buffer-namebuffer-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-namebuffer-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ではこの方法を使えない。 しかし、編集可能なバッファならば、この方法を使える。 コードや(本書のような)文書を書くときには、この機能はとても便利である。


Node:Getting Buffers, Next:, Previous:Buffer Names, Up:Practicing Evaluation

バッファの取得

関数buffer-nameは、バッファの名前を返す。 バッファそのものを取得するには、 別の関数current-bufferが必要である。 この関数をコードで使うと、バッファそのものを取得することになる。

名前とその名前が表すオブジェクトや実体とは互いに異なるものである。 読者自身は読者の名前ではない。 読者は、その名前で他人が参照する「人」である。 読者がGeorgeと話したいと頼んだときに、Georgeと文字が書かれたカードを渡されたら、驚くであろうが、 満足はしないであろう。 名前と話をしたいのではなくて、その名前で参照される人と話をしたいのである。 バッファも同様である。 一時的バッファの名前は*scratch*であるが、 名前そのものがバッファではない。 バッファそのものを得るにはcurrent-bufferのような関数を使う必要がある。

しかし、多少込み入った事情もある。 ここで試すように、式でcurrent-bufferを評価すると バッファの内容ではなくバッファの表示形式が表示される。 Emacsがこのように動作する理由は2つある。 バッファには数千もの行が含まれることもあるので、 これを簡便に表示するには長すぎる。 また、バッファの内容は同じであっても、名前が異なるバッファもありえるので、 それらを区別できる必要がある。

例を示そう。

(current-buffer)

いつものようにこの式を評価すると、 エコー領域には#<buffer *info*>と表示される。 バッファ名ではなく、バッファそのものを表す特殊な表示形式である。

数やシンボルはプログラムに入力できるが、 バッファの表示形式を入力することはできない。 バッファそのものを得る唯一の方法は、 current-bufferのような関数を使うことである。

関連する関数としてother-bufferがある。 これは、現在使用しているバッファの直前に選択していたバッファを返す。 たとえば、バッファ*scratch*から現在のバッファに切り替えた場合には、 other-bufferはバッファ*scratch*を返す。

つぎの式を評価してみよう。

(other-buffer)

エコー領域には、#<buffer *scratch*>、あるいは、 今のバッファに切り替えるまえのバッファの表示形式が表示される。


Node:Switching Buffers, Next:, Previous:Getting Buffers, Up:Practicing Evaluation

バッファの切り替え

関数other-bufferの目的は、バッファそのものを引数に必要とする 関数にバッファを与えることである。 別のバッファに切り替えるためother-bufferswitch-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)も使った。 リストの先頭のシンボルが関数であるようなリストを評価すると、 その関数を呼び出すのである。 この用語は、鉛管工を「呼ぶ」とパイプの洩れを修理してくれるのと同じように、 関数は、「呼び出す」と何かを行ってくれる実体であるという考え方からきている。


Node:Buffer Size & Locations, Next:, Previous:Switching Buffers, Up:Practicing Evaluation

バッファサイズとポイントの位置

最後に、buffer-sizepointpoint-minpoint-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は、 カレントバッファにおいて取ることが可能なポイントの最大値を返す。


Node:Evaluation Exercise, Previous:Buffer Size & Locations, Up:Practicing Evaluation

演習問題

適当なファイルを読み込み、その中ほどに移動する。 バッファ名、ファイル名、長さ、ファイル内での位置のそれぞれを調べてみよ。


Node:Writing Defuns, Next:, Previous:Practicing Evaluation, Up:Top

関数定義の書き方

リストを評価するとき、Lispインタープリタは、 リストの先頭のシンボルに関数定義が結び付けられているかどうかを調べる。 いいかえれば、シンボルが関数定義を指すかどうかを調べる。 そうならば、コンピュータは定義内の命令列を実行する。 関数定義を持つシンボルを、単に関数と呼ぶ (しかし、正確には、定義が関数であり、シンボルはそれを指すだけである)。


Node:Primitive Functions, Next:, Previous:Writing Defuns, Up:Writing Defuns

すべての関数は別の関数を用いて定義されているが、 プログラミング言語Cで書かれた少数の 基本操作(primitive)関数はそうではない。 関数の定義を書くときには、他の関数を構成部品として用いてEmacs Lispで書く。 そのとき使用する関数は、 それ自身Emacs Lispで(読者自身が)書いたものであったり、 Cで書かれた基本操作関数である。 基本操作関数は、Emacs Lispで書いたものとまったく同じように使え、 そのように動作する。 これらをCで書いてあるのは、Cが動く十分な能力を持ったコンピュータならば 容易にGNU Emacsを動作できるようにするためである。

強調しておくが、Emacs Lispでコードを書くとき、 Cで書いた関数の使い方とEmacs Lispで書いた関数の使い方とは区別しない。 両者の違いは無関係なのである。 わざわざ言及したのは、興味深いと考えたからである。 既存の関数がEmacs Lispで書いてあるのか、Cで書いてあるのかは、 特に調べない限りわからない。


Node:defun, Next:, Previous:Primitive Functions, Up:Writing Defuns

スペシャルフォームdefun

Lispでは、mark-whole-bufferのようなシンボルには、 関数として呼ばれたときにコンピュータが実行するコードが結び付けられている。 このコードを関数定義(function definition)と呼び、 シンボルdefundefine function(関数を定義する)の略)で始まる Lispの式を評価することで作成する。 defunは、その引数を通常のようには評価しないので、 スペシャルフォーム(special form)と呼ばれる。

以下の節では、mark-whole-bufferのようなEmacsのソースコードの 関数定義を調べる。 本節では、関数定義がどのようなものかを理解してもらうために、 簡単な関数定義を説明する。 例を簡単にするために、算術演算を使った関数定義を取り上げる。 算術演算を使った例が嫌いな人もいるであろうが、落胆しないでほしい。 残りの節で説明するコードには、算術演算や数学はほとんどない。 そのほとんどは、テキストを扱う。

関数定義は、単語defunに続く最大で5つの部分から成る。

  1. 関数定義を結び付けるシンボルの名前。
  2. 関数に渡される引数のリスト。 関数に引数を渡さない場合には、空リスト()を指定する。
  3. 関数の説明文 (省略できるが、付けることを強く推奨する)。
  4. M-xに続けて関数名を入力したり、適当なキー列をタイプして使えるように、 関数を対話的にするための式。 省略できる。
  5. コンピュータに動作を命じるコード。 関数定義の本体(body)

関数定義の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 fdescribe-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インタープリタに関数の定義を教える操作である。 次節では、インストールについて説明する。


Node:Install, Next:, Previous:defun, Up:Writing Defuns

関数定義のインストール

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 fdescribe-function)に続けて 関数名 multiply-by-sevenをタイプすれば、 関数の説明文を読むことができる。 これを行うと、つぎのような内容のウィンドウ*Help*が画面に現れる。

multiply-by-seven:
Multiply NUMBER by seven.

(画面を単一のウィンドウに戻すには、C-x 1とタイプする。)


Node:Change a defun, Previous:Install, Up:Install

関数定義の変更

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でコードを書くには、 関数を書いてインストールしてテストし、 必要に応じて、修正や機能強化して再インストールする。


Node:Interactive, Next:, Previous:Install, Up:Writing Defuns

関数を対話的にする

関数を対話的にするには、 関数の説明文のあとにスペシャルフォーム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つある。

  1. C-u 3 M-x forward-sentenceのように、 関数に渡すべき数を含む前置引数をタイプしてから、 M-xに続けて関数名をタイプする。 あるいは、
  2. C-u 3 M-eのような関数にバインドされたキー列をタイプする。

上のキー列の例は、どちらもポイントを文単位に3つ進める (multiply-by-sevenにはキーがバインドされていないので、 キーバインドを使う例としては使えない)。

(コマンドをキーにバインドする方法については See Keybindings。)

前置引数を対話的関数に渡すには、 M-3 M-eのようにキー<META>に続けて数をタイプするか、 C-u 3 M-eのようにC-uに続けて数をタイプする (数をタイプせずにC-uだけをタイプすると、デフォルトは4)。


Node:multiply-by-seven in detail, Previous:Interactive, Up:Interactive

対話的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が二重引用符なしでテキストを 表示するからである。)


Node:Interactive Options, Next:, Previous:Interactive, Up:Writing Defuns

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: "に続けて、\nrを 指定する。 こうすると、Emacsはバッファ名を問い合わせることに加えて、 ポイントとマークの値を関数に渡す。 つまり、引数は全部で3つである。

この場合、関数定義はつぎのようになり、 bufferstartendの各シンボルには、 バッファ、リージョンの開始位置、 終了位置の現在値を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


Node:Permanent Installation, Next:, Previous:Interactive Options, Up:Writing Defuns

コードの恒久的インストール

関数定義を評価して関数定義をインストールすると、 Emacsを終了するまでは関数定義は有効である。 新たにEmacsを起動したときには、 関数定義を再度評価するまでは関数定義はインストールされない。

新たにEmacsを起動するたびに、自動的にコードをインストールしたくなるであろう。 これにはいくつかの方法がある。

最後に、Emacsのすべてのユーザーが使いそうなコードならば、 ネットワークに投稿するかFree Software Foundationに送付する (こうする場合には、投稿するまえにコードに コピーレフトの注意書きを入れてほしい)。 Free Software Foundationにコードを送付すると、 Emacsの次期リリースにコードが含まれるかもしれない。 このような「寄贈行為」によりEmacsが成長してきたのである。


Node:let, Next:, Previous:Permanent Installation, Up:Writing Defuns

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)」の同義語である)。


Node:Parts of let Expression, Next:, Previous:let, Up:let

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 ((変数 )
      (変数 )
      ...)
      本体...)


Node:Sample let Expression, Next:, Previous:Parts of let Expression, Up:let

let式の例

つぎの式では、2つの変数zebratigerを作り、 それぞれに初期値を与える。 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つの変数は、zebratigerである。 各変数は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の位置に表示される。


Node:Uninitialized let Variables, Previous:Sample let Expression, Up:let

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を、 シンボルpinefirnilを、 シンボルoaksomeを束縛する。

letの最初の部分では、変数pinefirは括弧で囲んでない 単独のアトムである。 そのため、これらの変数は空リストnilに束縛される。 一方、oakは、リスト(oak 'some)の一部なので、 someに束縛される。 同様に、birchもリストの一部なので数3に束縛される (数はそれ自身に評価されるので、数をクオートする必要はない。 また、メッセージに数を表示するには%sのかわりに%dを使う)。 4つの変数をまとめてリストにすることで、letの本体と区別できるようにする。


Node:if, Next:, Previous:let, Up:Writing Defuns

スペシャルフォームif

defunletに続く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と表示される。


Node:type-of-animal in detail, Previous:if, Up:if

関数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の最初の使用例では、 引数fiercetype-of-animalに渡した。 fiercefierceに等しいので、 式(equal characteristic 'fierce)は真を返す。 すると、ifは第2引数、つまり、 真の場合の動作(message "It's a tiger!")を評価する。

一方、type-of-animalの2番目の使用例では、 引数zebratype-of-animalに渡した。 zebrafierceに等しくないので、真の場合の動作は評価されず、 if式はnilを返す。


Node:else, Next:, Previous:if, Up:Writing Defuns

If-then-else式

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!"と表示される。

characteristicferocious(凶暴な)であれば、 メッセージ"It's not fierce!"が表示されるが、これは誤解を招く。 コードを書く際には、ifで調べる値の可能な組み合わせを十分に考慮し、 そのようにプログラムを書く必要がある。)


Node:Truth & Falsehood, Next:, Previous:else, Up:Writing Defuns

Lispの真偽値

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)


Node:save-excursion, Next:, Previous:Truth & Falsehood, Up:Writing Defuns

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-xexchange-point-and-mark)を用いて カーソルをマークに移動するとともに、 カーソル移動前のポイント位置にマークを設定する。 さらに、別のマークが設定してあった場合には、 交換前のマークの位置をマークリングに保存する。 このようにして複数個のマーク位置を保存できる。 C-u C-<SPC>と数回タイプすると保存したマーク位置に移動できる。

バッファのポイントとマークのあいだの部分をリージョン(region)と呼ぶ。 center-regioncount-lines-regionkill-regionprint-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)。


Node:Template for save-excursion, Previous:save-excursion, Up:save-excursion

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
    本体...))


Node:Review, Next:, Previous:save-excursion, Up:Writing Defuns

復 習

これまでの章では、多数の関数やスペシャルフォームを紹介してきた。 以下には、説明しなかった同種の関数も含めて概要を記しておく。

eval-last-sexp
ポイントの現在位置の直前にあるシンボリック式を評価する。 引数を指定せずにこの関数を起動した場合には、エコー領域に値を表示する。 引数を指定した場合には、カレントバッファに結果を表示する。 このコマンドは慣習的にC-x C-eにバインドされる。
defun
関数を定義する。 このスペシャルフォームは、多くても5つの部分から成る。 つまり、名前、関数に渡される引数の雛型、 説明文、省略してもよい対話的使用の宣言、定義の本体である。

例:

(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
数値の前置引数(「p」は小文字)。
r
2つの数値引数でポイントとマークを渡す。 値が小さいほうを先に渡す。 これは、1つではなく2つの引数を渡す唯一のコード文字である。

コード文字の完全な一覧に ついては、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
関数の第1引数を評価する。 それが真ならば、第2引数を評価する。 そうでない場合、第3引数があればそれを評価する。

スペシャルフォーム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
2つのオブジェクトが同じであるかどうかを調べる。 equalは、2つのオブジェクトが同じ内容で同じ構造ならば真を返す。 一方、eqは、2つの引数が同一のオブジェクトならば真を返す。
<
>
<=
>=
関数<は、第1引数が第2引数より小さいかどうかを検査する。 対応する関数>は、第1引数が第2引数より大きいかどうかを検査する。 同様に、<=は、第1引数が第2引数より小さいか等しいかどうかを検査し、 >=は、第1引数が第2引数より大きいか等しいかどうかを検査する。 いずれの場合でも、2つの引数は数である必要がある。
message
エコー領域にメッセージを表示する。 メッセージは1行であること。 第1引数は文字列であり、文字列に続く引数の値を表示するために %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
Emacsが操作対象としているバッファを返す。 このバッファが画面に表示されているとは限らない。
other-buffer
other-bufferに引数として渡したバッファやカレントバッファ以外の) もっとも最近に選択していたバッファを返す。
switch-to-buffer
Emacsが操作対象とするバッファを指定し、同時に、 カレントウィンドウに表示してユーザーが見られるようにする。 通常、C-x bにバインドされる。
set-buffer
Emacsが操作対象とするバッファを切り替える。 ウィンドウの表示は変更しない。
buffer-size
カレントバッファ内にある文字数を返す。
point
バッファの先頭から現在のカーソル位置までの文字の個数を表す整数を返す。
point-min
カレントバッファで取りえるポイントの最小値を返す。 ナロイングしていない場合には、1である。
point-max
カレントバッファで取りえるポイントの最大値を返す。 ナロイングしていない場合には、バッファの最後である。


Node:defun Exercises, Previous:Review, Up:Writing Defuns

演習問題


Node:Buffer Walk Through, Next:, Previous:Writing Defuns, Up:Top

バッファ関連の関数

本章では、GNU Emacsで使われている数個の関数を詳しく調べよう。 つまり「ウォークスルー」をしてみよう。 これらの関数はLispコードの例題として取り上げたが、仮想的な例題ではない。 最初の簡略した関数定義を除いて、GNU Emacsで実際に使っているコードである。 これらの定義から多くのことを学べるはずである。 ここで説明する関数は、すべて、バッファに関連するものである。 それ以外の関数についてはのちほど説明する。


Node:Finding More, Next:, Previous:Buffer Walk Through, Up:Buffer Walk Through

詳しい情報を得る

このウォークスルーでは、個々の新しい関数を、あるときは詳しく、 あるときはその概要を説明する。 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の標準ライブラリを検索できる」のような記述がある。


Node:simplified-beginning-of-buffer, Next:, Previous:Finding More, Up:Buffer Walk Through

beginning-of-bufferの簡略した定義

コマンドbeginning-of-bufferには十分慣れていて理解しやすいであろうから、 この関数から始めよう。 対話的関数として使うと、beginning-of-bufferは、 バッファの先頭にカーソルを移動し、それまでカーソルがあった位置にマークを 設定する。 一般にはM-<にバインドしてある。

本節では、もっとも多く使われる形式に簡略した関数を説明する。 簡略版の関数は正しく動作するが、複雑なオプションを処理するコードは含まない。 別の節で、完全な関数を説明する (See beginning-of-buffer)。

コードを調べるまえに、関数定義には何が含まれるかを考えてみよう。 M-x beginning-of-bufferM-<のようなキー列で関数を呼べるように、 関数を対話的にする式を含んでいる必要がある。 バッファのもとの位置にマークを設定するコードが必要である。 バッファの先頭にカーソルを移動するコードも必要である。

では、簡略版の関数のコード全体を示そう。

(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つの部分がある。

  1. 名前。 ここでは、simplified-beginning-of-bufferである。
  2. 引数のリスト。 ここでは、空リスト()である。
  3. 説明文の文字列。
  4. 対話的にするための式。
  5. 本体。

この関数定義では、引数リストは空である。 つまり、この関数は引数を必要としない (関数の完全な定義では、省略可能な引数を取る)。

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))を使う。


Node:mark-whole-buffer, Next:, Previous:simplified-beginning-of-buffer, Up:Buffer Walk Through

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と 同様である。


Node:Body of mark-whole-buffer, Previous:mark-whole-buffer, Up:mark-whole-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のときとまったく同じに書いてある。 この式では、カーソルをバッファの最小ポイント つまり、バッファの先頭(あるいは、バッファの参照可能な部分の先頭)に移動する。 この結果、バッファの先頭にポイントがあり、バッファの最後にマークが設定される。 したがって、バッファ全体がリージョンとなる。


Node:append-to-buffer, Next:, Previous:mark-whole-buffer, Up:Buffer Walk Through

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である。

関数のつぎの部分は説明文であるが、これは明白であろう。


Node:append interactive, Next:, Previous:append-to-buffer, Up:append-to-buffer

append-to-bufferinteractive

関数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つの引数(つまり、startend)に ポイントとマークの値を束縛するようにEmacsに指示する。


Node:append-to-buffer body, Next:, Previous:append interactive, Up:append-to-buffer

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つの要素がある。

  1. シンボルlet
  2. 変数リスト。 ここでは、1つの2要素リスト(variable value)
  3. 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))の始まりを表す ことに注意してほしい。


Node:append save-excursion, Previous:append-to-buffer body, Up:append-to-buffer

append-to-buffersave-excursion

append-to-bufferlet式の本体は、 save-excursion式から成っている。

関数save-excursionは、ポイントとマークの位置を記録し、 save-excursionの本体の式の実行を完了すると これらの位置を復元する。 さらに、save-excursionはもとのバッファも記録しておき復元する。 append-to-bufferではこのようにsave-excursionを使う。

複数行にまたがるリストは、最初のシンボル以外は最初のシンボルよりも 字下げして書き、Lisp関数もこのように書く。 この関数定義では、つぎに示すように、letdefunよりも字下げし、 save-excursionletよりも字下げする。

(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 (oldbufcurrent-bufferの値を束縛する)
  (save-excursion                       ; バッファを記録する
    バッファを切り替える
    oldbufの文字列をバッファへコピーする)

  もとのバッファへ戻す
終了するときに名前oldbufを消す

まとめると、append-to-bufferはつぎのように動作する。 変数oldbufにカレントバッファを記録する。 必要ならば作成して新しいバッファを取得し、そのバッファへ切り替える。 oldbufの値を用いて、もとのバッファのリージョンのテキストを 新しいバッファに挿入する。 save-excursionを使っているので、もとのバッファに戻る。

append-to-bufferを調べることで、ある程度複雑な関数について 知ることができた。 letsave-excursionの使い方や、 バッファを切り替えたあとで、もとのバッファに戻す方法がわかったと思う。 多くの関数で、letsave-excursionset-buffer をこのように使う。


Node:Buffer Related Review, Next:, Previous:append-to-buffer, Up:Buffer Walk Through

復 習

本章で説明したさまざまな関数の概要をまとめておく。

describe-function
describe-variable
関数や変数の説明文を表示する。 慣習的に、C-h fC-h vにバインドされる。
find-tag
関数や変数を含むソースファイルを探し、そのファイルのバッファに切り替え、 その関数や変数の先頭にポイントを移動する。 慣習的に、M-.(つまり、<META>キーに続けてピリオド) にバインドされる。
save-excursion
ポイントとマークの位置を記録し、save-excursionの引数を評価し終えたら、 もとの値に戻す。 カレントバッファも記録しておき、終了後にもとに戻す。
push-mark
指定位置にマークを設定し、設定前のマークの値をマークリングに保存する。 マークはバッファ内の位置であり、バッファにテキストが追加されても 削除されても、その相対的な位置を保持する。
goto-char
引数で指定した位置にポイントを移動する。 引数は、数、マーク、(point-min)のような位置を返す式のいずれかである。
insert-buffer-substring
引数として関数に渡されたバッファのリージョンのテキストのコピーを カレントバッファに挿入する。
mark-whole-buffer
バッファ全体をリージョンとする。 通常、C-x hにバインドされる。
set-buffer
Emacsの注意を別のバッファに向けるが、ウィンドウへの表示は変えない。 プログラムで別のバッファを操作するときに使う。
get-buffer-create
get-buffer
指定した名前のバッファを探し、存在しなければその名前のバッファを作成する。 関数get-bufferは、指定した名前のバッファが 存在しなければnilを返す。


Node:Buffer Exercises, Previous:Buffer Related Review, Up:Buffer Walk Through

演習問題


Node:More Complex, Next:, Previous:Buffer Walk Through, Up:Top

多少複雑な関数

本章では、前章で学んだことをもとに、多少複雑な関数を見てみよう。 関数copy-to-bufferでは、1つの定義内で式save-excursionを 2つ使う方法を、関数insert-bufferでは、 interactive式での*の使い方とorの使い方を、 名前と名前が参照するオブジェクトとの重要な違いを示す。


Node:copy-to-buffer, Next:, Previous:More Complex, Up:More Complex

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 (oldbufcurrent-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がある)。


Node:insert-buffer, Next:, Previous:copy-to-buffer, Up:More Complex

insert-bufferの定義

insert-bufferもバッファに関連した関数である。 このコマンドは、別のバッファからカレントバッファへコピーする。 カレントバッファのテキストのリージョンを別のバッファへコピーする append-to-buffercopy-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: ")
  本体...)


Node:insert interactive expression, Next:, Previous:insert-buffer, Up:insert-buffer

insert-bufferinteractive

insert-bufferでは、interactiveの引数には、 アスタリスク*bInsert buffer: の2つの部分がある。


Node:read-only buffer, Next:, Previous:insert interactive expression, Up:insert interactive expression

読み出し専用バッファ

アスタリスクは、読み出し専用バッファ、つまり、 変更できないバッファに対処するためである。 読み出し専用のバッファにinsert-bufferを使うと、 読み出し専用である旨のメッセージがエコー領域に表示され、 端末のベルが鳴るか点滅する。 カレントバッファには挿入できないのである。 アスタリスクのあとにはつぎの部分との区切りに改行は必要ない。


Node:b for interactive, Previous:read-only buffer, Up:insert interactive expression

interactive式のb

interactive式の引数の2番目の部分は、小文字のbで始まっている (大文字のBで始まるappend-to-bufferのコードと異なる See append-to-buffer)。 小文字のbは、insert-bufferの引数は既存のバッファであるか バッファ名であることをLispインタープリタに指示する (大文字のBでは、バッファが存在しない場合も許す)。 Emacsは、デフォルトのバッファを提示してバッファ名を問い合わせ、 名前の補完も行う。 バッファが存在しないと、メッセージ「No match(一致するものがない)」を表示して 端末のベルを鳴らす。


Node:insert-buffer body, Next:, Previous:insert interactive expression, Up:insert-buffer

関数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で書き換えて、 すでによく知っている方法で考えてみよう。


Node:if & or, Next:, Previous:insert-buffer body, Up:insert-buffer

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にバッファそのものを設定し、 (バッファ名であった)まえの値を置き換える。


Node:insert or, Next:, Previous:if & or, Up:insert-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 (招待客の腕をとっている) (招待客を探し腕をとる))


Node:insert let, Previous:insert or, Up:insert-buffer

insert-bufferlet

変数bufferがバッファ名ではなくバッファそのものを参照することを 保証したあと、関数insert-bufferlet式に進む。 ここには、3つのローカル変数、startendnewmarkがあり、 それぞれを初期値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-minpoint-maxを用いて バッファの先頭と最後を変数startendに設定する。 ここでは、setqにより、1つの式で2つの変数に値を設定する方法も 示している。 setqの最初の引数には第2引数の値が、第3引数には第4引数の値が設定される。

内側のsave-excursionの本体を終了すると、 save-excursionはもとのバッファに戻すが、 startendには、 テキストのコピーもとのバッファの先頭と最後の値が設定されたままである。

外側のsave-excursion式の概要はつぎのとおりである。

(save-excursion
  (内側の式save-excursion
     (新しいバッファに切り替えstartendを設定)
  (insert-buffer-substring buffer start end)
  (setq newmark (point)))

関数insert-buffer-substringは、 バッファbufferの始めstartから終わりendまでのリージョンの テキストをカレントバッファにコピーする。 startendのあいだは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は、 letsave-excursionset-bufferを使っている。 さらに、この関数ではorの1つの使い方を示した。 これらのすべての関数は、何度も何度も使う構成部品である。


Node:beginning-of-buffer, Next:, Previous:insert-buffer, Up:More Complex

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は、引数を指定しても、しなくても呼べる。 つまり、引数は省略できる。


Node:Optional Arguments, Next:, Previous:beginning-of-buffer, Up: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である。 argnil以外の値があるのは、 引数を指定してbeginning-of-bufferが呼ばれた場合であり、 if式の真の場合の動作を評価する。 一方、引数なしでbeginning-of-bufferを呼ぶと、 argの値はnilとなりif式の偽の場合の動作が評価される。 偽の場合の動作は単純であり、point-minである。 このように評価される場合は、goto-char式全体は (goto-char (point-min))となり、 関数beginning-of-bufferの簡略な場合と同じである。


Node:beginning-of-buffer opt arg, Next:, Previous:Optional Arguments, Up: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つの場合がある。


Node:large-case, Next:, Previous:beginning-of-buffer opt arg, Up:beginning-of-buffer opt arg

大きなバッファでの動作

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

これにより、望みの位置にカーソルが移動する。


Node:small-case, Previous:large-case, Up:beginning-of-buffer opt arg

小さなバッファでの動作

バッファが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に渡してカーソルを移動する。


Node:beginning-of-buffer complete, Previous:beginning-of-buffer opt arg, Up:beginning-of-buffer

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の位置にカーソルを位置決めして 見栄えをよくするためのもので、必要ないことかもしれないが、 こうしないと不満のもとになる。


Node:Second Buffer Related Review, Next:, Previous:beginning-of-buffer, Up:More Complex

復 習

本章で説明したことがらをまとめておく。

or
各引数を順番に評価し、nil以外の値を返した最初の値を返す。 nil以外の値を返すものがなければ、nilを返す。 つまり、引数の最初の真の値を返す。 1つでも真のものがあれば、真の値を返す。
and
各引数を順番に評価し、すべてがnilならばnilを返す。 nilでないものがなければ、最後の引数の値を返す。 つまり、すべての引数が真ならば真の値を返す。
&optional
関数定義に引数を省略できることを表すキーワード。 つまり、必要ならば、引数を与えなくても関数を評価できる。
prefix-numeric-value
(interactive "P")で得た「生の前置引数」を数値に変換する。
forward-line
ポイントをつぎの行の先頭に移動する。 1より大きな引数を与えた場合には、その行数だけポイントを進める。 ポイントを指定量だけ進められない場合には、 forward-lineは可能なだけ進めて、指定量に満たない行数を返す。
erase-buffer
カレントバッファの全内容を削除する。
bufferp
引数がバッファならtを、さもなければnilを返す。


Node:&optional Exercise, Previous:Second Buffer Related Review, Up:More Complex

&optional引数の演習問題

省略できる引数に指定した数がfill-columnの値に比べて 大きいか小さいかをメッセージに表示する対話的関数を書いてみよ。 ただし、関数に引数を渡されなかった場合のデフォルトは56とする。


Node:Narrowing & Widening, Next:, Previous:More Complex, Up:Top

ナロイングとワイドニング

ナロイングとは、バッファの特定部分に集中して残りの部分を 不用意に変更しないようにするEmacsの機能である。 初心者に混乱を与えないように、ナロイングは、通常、無効にしてある。


Node:narrowing advantages, Next:, Previous:Narrowing & Widening, Up:Narrowing & Widening

ナロイングすると、バッファの残りの部分は存在しないかのように見えなくなる。 たとえば、バッファのある部分のみで単語を置き換えるような場合には、 利点となる。 望みの範囲にナロイングすれば、その部分だけで置き換えを行える。 文字列検索もその範囲内のみで行えるので、 たとえば、文書のある部分を編集する場合、編集範囲にナロイングしておけば それ以外の範囲の文字列を拾うことがない。

しかし、ナロイングするとバッファの残りの部分が見えなくなるため、 偶然にナロイングしてしまうとファイルのその部分を削除してしまったと思うような 読者を怯えさせる。 さらに、(通常C-x uにバインドされる)コマンドundoでも、 ナロイングを無効にしない(また、そうすべきでもない)ので、 コマンドwidenによりバッファの残りの部分が見えるようになることを 知らない人は絶望することになる (Emacsの第18版では、widenC-x wに、 第19版では、C-x n wにバインドしてある)。

ナロイングは人間にとってと同様にLispインタープリタにとっても有用である。 Emacs Lisp関数は、しばしば、バッファの一部分に働くように設計されている。 逆に、ナロイングしてあるバッファの全体に働く必要があるEmacs Lisp関数もある。 たとえば、関数what-lineは、ナロイングが有効な場合にはそれを無効にし、 処理を終了するともとのようにナロイングする。 一方で、what-lineからも呼ばれる関数count-linesでは、 ナロイングを用いてバッファの操作範囲を限定し、終了するときにもとに戻す。


Node:save-restriction, Next:, Previous:narrowing advantages, Up:Narrowing & Widening

スペシャルフォーム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-excursionsave-restrictionを続けて使う場合には、 save-excursionは外側で使うべきである。 逆順に使うと、save-excursionを呼んでEmacsが切り替えたバッファの ナロイングを記録し損なうことがある。 つまり、save-excursionsave-restrictionとを一緒に使う場合には、 つぎのように書く。

(save-excursion
  (save-restriction
    本体...))


Node:what-line, Next:, Previous:save-restriction, Up:Narrowing & Widening

what-line

コマンドwhat-lineは、カーソルが位置する行の行数を調べる。 この関数では、コマンドsave-restrictionsave-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-restrictionwidenを使っている。

スペシャルフォームsave-restrictionは、 カレントバッファに設定されているナロイングを記録し、 save-restrictionの本体のコードを評価したあと、 記録したナロイングを復元する。

スペシャルフォームsave-restrictionに続いて、widenがある。 この関数は、what-lineが呼ばれたときにカレントバッファに設定されている ナロイングを無効にする (このナロイングはsave-restrictionが記録している)。 このワイドニングにより、バッファの先頭から行数を数えられるようになる。 さもないと、参照可能なリージョン内に制限されてしまう。 設定されていたもとのナロイングは、 スペシャルフォームsave-restrictionを終了する直前に復元される。

widenの呼び出しに続いて、save-excursionがあり、 カーソル(つまり、ポイント)とマークの位置を記録し、 save-excursionの本体のコードでポイントを移動する 関数beginning-of-lineを呼んだあとにそれらを復元する。

(式(widen)は、save-restrictionsave-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はもとのナロイングの設定を復元する。


Node:narrow Exercise, Previous:what-line, Up:Narrowing & Widening

ナロイングの演習問題

バッファの後半にナロイングしていて最初の行を参照できないような場合であっても、 カレントバッファの最初の60文字を表示する関数を書いてみよ。 これには、save-restrictionwidengoto-charpoint-minbuffer-substringmessageを始め、 さまざまな関数をポプリのように混ぜ合わせて使う必要がある。


Node:car cdr & cons, Next:, Previous:Narrowing & Widening, Up:Top

基本関数 carcdrcons

Lispでは、carcdrconsは基本関数である。 関数consはリストの作成、 関数carcdrはリストの分解に使う。

関数copy-region-as-killのウォークスルーでは、 consに加えてcdrの変形であるsetcdrnthcdrを 見ることになる (See copy-region-as-kill)。


Node:Strange Names, Next:, Previous:car cdr & cons, Up:car cdr & cons

関数consの名前は不合理ではなく、 単語「construct(作り上げる)」の省略である。 一方、carcdrの名前の由来は、難解である。 carは「Contents of the Address part of the Register」の頭文字であり、 cdr(「クダー」と読む)は 「Contents of the Decrement part of the Register」の頭文字である。 これらの語句は、最初のLispが開発された初期のコンピュータの ハードウェアの一部を指す。 この語句は古くて意味がないばかりでなく、 Lispに関していえば、25年間以上にもわたってこれらの語句は無意味であった。 研究者の一部には、これらの関数に対する合理的な名称を使う人もいるが、 それにもかかわらず、これらの名称は使われ続けている。 特に、これらはEmacs Lispのソースコードでも使われているので、 本書でもこれにならう。


Node:car & cdr, Next:, Previous:Strange Names, Up:car cdr & cons

carcdr

リストの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であろう。

(つぎのことに注意してほしい: 新しい関数に名前を付けるときには、何をしているかを注意深く考えてほしい。 というのは、予想以上に長期間にわたって使われることもあるからである。 本書で、(carcdrのような)これらの名称を使い続けるのは、 Emacs Lispのソースコードでこれらを使っているからである。 同じ名称を使わないと、読者がコードを読む際に苦労するであろう。 合理的な名称を使えば、あとに続く人々に感謝されるはずである。)

(pine fir oak maple)のようなシンボルだけから成るリストに carcdrを適用すると、関数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)))

再度指摘するが、carcdrは、非破壊的である。 つまり、これらをリストに適用したあとでも、 リストは変更されていないことに注意してほしい。 これらの使い方において、この性質はとても重要である。

第1章のアトムに関する説明で、Lispにおいては、 「配列などのある種のアトムは分解できるが、 その機構はリストを分解する機構とは異なる。 Lispでは、リストのアトムを分解することはできない」と述べた (See Lisp Atoms)。 関数carcdrは、リストを分解するために使い、 Lispにとって基本的であると考えられている。 これらの関数では、 配列を分解したりその一部を参照できないので、配列はアトムと考えられる。 逆に、別の基本関数consはリストを作り上げるが、配列を作ることはできない (配列は、配列専用の関数で処理する。 See Arrays)。


Node:cons, Next:, Previous:car & cdr, Up:car cdr & cons

cons

関数consはリストを作り、carcdrの逆操作である。 たとえば、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番目の例では、daisybuttercupのまえにvioletを置いて 3要素リストを作っている。


Node:length, Previous:cons, Up:cons

リストの長さ: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


Node:nthcdr, Next:, Previous:cons, Up:car cdr & cons

nthcdr

関数nthcdrは、関数cdrに関連しており、 リストのcdrを繰り返し取る。

リスト(pine fir oak maple)cdrを取ると、 リスト(fir oak maple)を得る。 同じことを返された値に適用するとリスト(oak maple)を得る (もちろん、もとのリストにcdrを繰り返し適用しても、 関数はリストを変更しないので、同じ結果を得るだけである。 cdrcdrのように評価する必要がある)。 これを続けると、最終的には空リストを得ることになるが、 ()のかわりに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も もとのリストを変更しないことである。 つまり、関数は非破壊的である。 これは、関数setcarsetcdrとは大きく異なる。


Node:setcar, Next:, Previous:nthcdr, Up:car cdr & cons

setcar

名前から予想できるように、関数setcarsetcdrは、 リストのcarcdrに新しい値を設定する。 もとのリストを変更しないcarcdrと異なり、 これらはもとのリストを変更する。 実際にその動作を試してみよう。 まず、関数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は、giraffehippopotamusで置き換え、 リストを変更したのである。


Node:setcdr, Next:, Previous:setcar, Up:car cdr & cons

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)に変わったのである。


Node:cons Exercise, Previous:setcdr, Up:car cdr & cons

演習問題

consを使った数個の式を評価して、鳥のリストを作ってみよ。 リストにそのリスト自身をconsするとどうなるかを調べてみよ。 鳥の4要素リストの先頭要素を魚に置き換えてみよ。 さらに、そのリストの残りの部分を他の魚で置き換えてみよ。


Node:Cutting & Storing Text, Next:, Previous:car cdr & cons, Up:Top

テキストのカットと保存

GNU Emacsで「キル(kill)」コマンドでバッファからテキストをカットする (切り取る)と、それらはリストに保存され、 「ヤンク(yank)」コマンドで取り出せる。

(Emacsにおける単語「kill」は、実体の値を破壊しない処理を意味するが、 その用法は、歴史的な不運な偶然による。 もっと適切な用語は、キルコマンドの動作からすれば、 「clip(切り取る)」であろう。 バッファからテキストを切り取り、復元可能なように保存するからである。 Emacsのソースのすべての「kill」を「clip」に、 すべての「killed」を「clipped」に置き換えたくなる誘惑にしばしば駆られる。)


Node:Storing Text, Next:, Previous:Cutting & Storing Text, Up:Cutting & Storing Text

バッファからテキストを切る取ると、それはリストに保存される。 順に切り取ったテキストは順にリストに保存されるので、 リストはつぎのようになる。

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

関数carnthcdrを使えば、テキストの望みの断片を取り出せる。 たとえば、つぎのコードでは、 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


Node:zap-to-char, Next:, Previous:Storing Text, Up:Cutting & Storing Text

zap-to-char

関数zap-to-charは、GNU Emacsの第18版と第19版とでは書き方が異なる。 第19版での実装のほうがいくぶん簡単であるが、動作も少々異なる。 第19版の関数を説明してから、第18版の関数を説明する。

Emacs第19版の対話的関数zap-to-charの実装では、 カーソル(つまりポイント)の位置から指定文字を含めたテキストを取りさる。 zap-to-charで取りさったテキストはキルリングに保存され、 C-yyank)とタイプするとキルリングから取り出せる。 コマンドに引数を与えると、指定文字のその回数までのテキストを取りさる。 たとえば、「Thus, if the cursor were at ...」 の先頭にカーソルがあり、文字sを指定するとThusを取りさる。 引数に2を与えるとcursorsまでを含めて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))))


Node:zap-to-char interactive, Next:, Previous:zap-to-char, Up:zap-to-char

interactive

コマンドzap-to-charinteractive式はつぎのとおりである。

(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へ渡す正しい型の引数を 準備する。


Node:zap-to-char body, Next:, Previous:zap-to-char interactive, Up:zap-to-char

zap-to-charの本体

関数zap-to-charの本体には、 カーソルの現在位置から指定文字を含んだテキストをキル(つまり、削除)する コードがある。 コードの最初の部分はつぎのとおりである。

(kill-region (point) ...

(point)はカーソルの現在位置である。

コードのつぎの部分は、prognを使った式である。 prognの本体は、search-forwardpointの呼び出しから成る。

search-forwardを説明してからのほうが prognの動作を理解しやすいので、 search-forwardを説明してからprognを説明する。


Node:search-forward, Next:, Previous:zap-to-char body, Up:zap-to-char

関数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つの引数を取る。

  1. 第1引数は探す対象となるものであり、 "z"のような文字列である必要がある。

    zap-to-charに渡される引数は単独の文字である。 コンピュータの構成方法のために、Lispインタープリタは単独の文字と 文字列を区別する。 コンピュータ内部では、単独の文字は、1文字の文字列とは異なる電気的な形式となる (単独の文字は、コンピュータ内部では、しばしば、ちょうど1バイトで記録される。 一方、文字列は長かったり短かったりするので、コンピュータはそれに対応できる 必要がある)。 関数search-forwardは文字列を探すので、 関数zap-to-charが引数として受け取った文字は、 コンピュータ内部では、ある形式から別の形式に変換する必要がある。 さもないと、関数search-forwardは失敗する。 関数char-to-stringを使って、この変換を行う。

  2. 第2引数は探索範囲を限定し、バッファ内の位置を指定する。 この場合、バッファの最後まで探索してよいので、 探索範囲を限定せず、第2引数はnilである。
  3. 第3引数は、探索に失敗した場合にどうするかを指定する。 エラーを通知する(かつ、メッセージを表示する)か、nilを返す。 第3引数にnilを指定すると、探索に失敗すると関数はエラーを通知する。
  4. search-forwardの第4引数は、繰り返し回数、 つまり、文字列の出現を何回探すかを指定する。 この引数は省略でき、繰り返し回数を指定しないと1である。 この引数が負の場合には、逆向きに探索する。

search-forward式の概略はつぎのとおりである。

(search-forward "探索文字列"
                探索範囲
                探索失敗時の動作
                繰り返し回数)

では、prognを説明しよう。


Node:progn, Next:, Previous:search-forward, Up:zap-to-char

関数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に渡される。


Node:Summing up zap-to-char, Next:, Previous:progn, Up:zap-to-char

zap-to-charのまとめ

search-forwardprognの動作がわかったので、 関数zap-to-char全体としての動作を理解しよう。

kill-regionの第1引数は、コマンドzap-to-charを与えたときの カーソルの位置、つまり、そのときのポイントの値である。 prognの中で、探索関数が削除する文字の直後にポイントを移動し、 pointがその位置の値を返す。 関数kill-regionは、ポイントの2つの値を組み合わせて、 最初のものをリージョンの始まり、 あとのものをリージョンの終わりと解釈してリージョンを削除する。

search-forwardpointを2つ続けて余分な2つの引数として書くと、 2つの引数を取るコマンドkill-regionは失敗するので、 関数prognが必要なのである。 progn式は、kill-regionに対しては1つの引数となり、 kill-regionが第2引数に必要とする1つの値を返す。


Node:v-18-zap-to-char, Previous:Summing up zap-to-char, Up:zap-to-char

第18版の実装

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-charzまでのテキストを削除すると、 この版ではzは削除しない。 そのため、指定文字が削除されないようにポイントを移動する必要がある。


Node:progn body, Previous:v-18-zap-to-char, Up:v-18-zap-to-char

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版の実装より少々小さく、 より簡単である。


Node:kill-region, Next:, Previous:zap-to-char, Up:Cutting & Storing Text

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-regioncopy-region-as-killを使っていることである。


Node:delete-region, Next:, Previous:kill-region, Up:Cutting & Storing Text

delete-region:Cへ回り道

コマンドzap-to-charは関数kill-regionを使い、 それはさらにcopy-region-as-killdelete-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の本体はつぎの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に関する限り、beは、削除すべきバッファの先頭と最後を 表す2つの32ビット整数である。 しかし、Emacs Lispの他の数と同様に、32ビットの内の24ビットのみを使っている。 残りの8ビットは、情報の型の記録やその他の目的に利用される (ある種のマシンでは、6ビットのみを使う)。 ここでは、これらの数がバッファの位置を表すことを示すために8ビットを使う。 数の中のビットをこのように使うことをタグ(tag)と呼ぶ。 各32ビット整数で8ビットタグを利用すると、タグを利用しない場合に比べて 高速に動作するようにEmacsを書くことができる。 一方で、数は24ビットの範囲に制限されるため、Emacsのバッファは 約8メガバイトに制限される (コンパイルするまえに、ファイルemacs/src/config.hVALBITSGCTYPEBITSの定義を追加すれば、 バッファの最大サイズを増加できる。 Emacsディストリビューションに含まれるファイル emacs/etc/FAQの注意書きを参照してほしい)。

XINTは、32ビット長のLispオブジェクトから24ビット整数を取り出す Cのマクロである。 他の目的に用いる8ビットは捨てられる。 したがって、del_range (XINT (b), XINT (e))は、 bで始まりeで終わるリージョンを削除する。

Lispを書く人の観点からは、Emacsは非常に簡単であるが、 その背後には、すべてが正しく働くように、とても複雑なことが隠れている。


Node:defvar, Next:, Previous:delete-region, Up:Cutting & Storing Text

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


Node:copy-region-as-kill, Next:, Previous:defvar, Up:Cutting & Storing Text

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")
  本体...)

引数はbegendであり、 "r"が指定された対話的関数であるので、 2つの引数はリージョンの始まりと終わりを参照する必要がある。 本書を始めから読んでいる読者ならば、関数のこの部分を理解するのは簡単であろう。

単語「kill」が通常の意味と異なることを思い出せば、 説明文の内容に混乱することもないであろう。

関数の本体はif節で始まる。 この節で、2つの異なる場合を分ける。 コマンドkill-regionの直後に、このコマンドが実行されたかどうかを区別する。 そうならば、直前にコピーしたテキストに新たなリージョンを追加する。 さもなければ、直前の断片とは独立したテキストの断片として キルリングの先頭に挿入する。

関数の最後の2行は、2つのsetq式である。 1つは、変数this-commandkill-regionを設定し、 もう1つでは、変数kill-ring-yank-pointerがキルリングを指すようにする。

copy-region-as-killの本体は詳しく説明する価値がある。


Node:copy-region-as-kill body, Previous:copy-region-as-kill, Up:copy-region-as-kill

copy-region-as-killの本体

関数copy-region-as-killは、 連続してキルしたテキストは1つの断片にまとめるように書かれている。 キルリングからテキストを取り出すと、1つの断片として得ることになる。 さらに、カーソルの現在位置から終わりに向けたキルでは 直前にコピーしたテキストの末尾に追加し、 先頭向けのコピーでは直前にコピーしたテキストの先頭に追加する。 このようにして、テキスト内の語順は正しく保たれる。

この関数では、現在と直前のEmacsコマンドを記録する2つの変数を用いる。 this-commandlast-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つの式の構造と内容が同じかどうかを検査する。


Node:kill-append function, Next:, Previous:copy-region-as-kill body, Up:copy-region-as-kill body

関数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" ...


Node:copy-region-as-kill else-part, Previous:kill-append function, Up:copy-region-as-kill body

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」をリストに追加したのである。

以上は、関数内でsetqconsとが行うことと同じであるが、 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つの関数nthcdrsetcdrを使う。

setcdrについてはすでに説明した(see setcdr)。 setcarがリストのCARを設定するように、 setcdrはリストのCDRを設定する。 しかし、ここではsetcdrはキルリング全体のcdrを設定するのではない。 関数nthcdrが使われていて、キルリングの最後の要素の直前のcdrを 設定するのである。 つまり、最後の要素の直前のcdrはキルリングの最後の要素であるから、 キルリングの最後の要素を設定することになる。

関数nthcdrは、リストのCDRを繰り返し取るように動作する。 つまり、CDRCDRCDR...CDRを取る。 N回繰り返した結果を返す。

したがって、たとえば、4要素リストを3要素リストにするには、 最後の要素の直前のCDRnilにしてリストを短くすればよい。

つぎの3つの式を順に評価すれば、これを理解できるであろう。 まず、treesの値として(maple oak pine birch)を設定する。 つぎに、2つめのCDRCDRnilにしてから、 treesの値を見てみる。

(setq trees '(maple oak pine birch))
     => (maple oak pine birch)

(setcdr (nthcdr 2 trees) nil)
     => nil

trees
     => (maple oak pine)

setcdr式が返す値は、CDRnilに設定したので、 nilである。)

copy-region-as-killでは、関数nthcdrは、 キルリングの許された最大長引く1回だけCDRを取り、 その要素(キルリングの残りの要素)のCDRnilを設定する。 これにより、キルリングが長くなり過ぎるのを防ぐ。

関数copy-region-as-killの最後の行の直前はつぎのとおりである。

(setq this-command 'kill-region)

この行は、if式の内側でも外側でもなく、 copy-region-as-killが呼ばれるごとに評価される。 ここが、this-commandkill-regionを設定する場所である。 すでに説明したように、つぎにコマンドが与えられると、 変数last-commandにはこの値が設定される。

関数copy-region-as-killの最後の行は、つぎのとおりである。

(setq kill-ring-yank-pointer kill-ring)

kill-ring-yank-pointerはグローバル変数であり、 kill-ringと同じに設定される。

kill-ring-yank-pointerはポインタと名前が付いているが、 キルリングと同じ変数である。 しかし、変数の使われ方が人間にわかりやすいように、この名称が選ばれた。 この変数は、yankyank-popなどの関数で使われる (see Yanking)。

これで、バッファから切り取ったテキストを復元する ヤンクコマンドのコードの説明に進める。 しかし、ヤンクコマンドを説明するまえに、コンピュータでのリストの実装方法を 学んでおくのがよいであろう。 そうすれば、「ポインタ」などの用語の不可解さが明らかになる。


Node:cons & search-fwd Review, Next:, Previous:copy-region-as-kill, Up:Cutting & Storing Text

復 習

これまでに説明した関数のいくつかを以下にまとめておく。

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つの引数を取る。

  1. 探すべき文字列。
  2. 探索範囲の制限。 省略できる。
  3. 探索に失敗した場合にnilを返すかエラーメッセージを返すか指定する。 省略できる。
  4. 探索を何回行うかを指定する。 省略できる。 負の場合には、逆向きに(先頭へ向けて)探索する。

kill-region
delete-region
copy-region-as-kill
kill-regionは、ポイントとマークのあいだのテキストを バッファから切り取り、ヤンクで復元できるように、 キルリングにそのテキストを保存する。

delete-regionは、ポイントとマークのあいだのテキストを バッファから取りさり、破棄する。 復元することはできない。

copy-region-as-killはポイントとマークのあいだのテキストを キルリングにコピーし、キルリングからそのテキストを復元できるようにする。 この関数は、バッファからテキストを取りさったりしない。


Node:search Exercises, Previous:cons & search-fwd Review, Up:Cutting & Storing Text

探索の演習問題


Node:List Implementation, Next:, Previous:Cutting & Storing Text, Up:Top

リストの実装方法

Lispでは、アトムは単純な方法で記録されている。 現実の実装が単純ではないとしても、理論的には単純である。 たとえば、アトムroseは、roseの4つの連続した文字として記録されている。 一方で、リストは異なった方法で記録されている。 その機構は同様に単純であるが、その考え方に慣れるには時間がかかる。 リストは一連のポインタ対を用いて記録されている。 ポインタ対の最初のポインタはアトムやリストを指し、 ポインタ対の2番目のポインタはつぎの対やシンボル、 あるいは、リストの終わりを表すnilを指す。

ポインタ自身は、とても単純で、それが指すもののアドレスである。 したがって、リストは一連のアドレス対として記録される。

たとえば、リスト(rose violet buttercup)には3つの要素、 rosevioletbuttercupがある。 コンピュータ内部では、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を設定しても、リスト自体は変わらない。 シンボルはリストを辿ったアドレスを持つだけである (専門用語では、CARCDRは「非破壊的」である)。 したがって、つぎの式を評価すると

(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の変数に値を設定すると、 変数が参照するリストのアドレスが設定される。


Node:List Exercise, Previous:List Implementation, Up:List Implementation

演習問題

flowersvioletbuttercupを設定せよ。 このリストにさらに2つの花をコンスし、 この新たなリストをmore-flowersに設定せよ。 flowersCARに魚を設定せよ。 このとき、more-flowersはどんなリストを持つか?


Node:Yanking, Next:, Previous:List Implementation, Up:Top

テキストの取り出し方

GNU Emacsで「キル」コマンドでバッファからテキストを切り取ったときには、 「ヤンク」コマンドでそのテキストを取り出せる。 バッファから切り取ったテキストはキルリングに保存され、 ヤンクコマンドはキルリングの適当な内容をバッファに挿入する (同じバッファである必要はない)。

簡単なコマンドC-yyank)では、キルリングの先頭要素を カレントバッファに挿入する。 C-yの直後にM-yを続けると、先頭要素を2番目の要素で置き換える。 さらにM-yを続けると、2番目の要素を3番目の要素で、4番目の要素で、 5番目の要素で置き換えるというようになる。 キルリングの最後の要素に達した場合には、一巡して先頭要素で置き換える (これが、キルリングを「リスト」とは呼ばずに「リング」と呼ぶ理由である。 しかし、テキストを保持する実際のデータ構造はリストである。 リストをリングとして扱う方法の詳細は、See Kill Ring)。


Node:Kill Ring Overview, Next:, Previous:Yanking, Up:Yanking

キルリングの概要

キルリングは、文字列のリストである。 これはつぎのようになっている。

("some text" "a different piece of text" "yet more text")

これが筆者のキルリングの内容であったとすると、 C-yとタイプするとカレントバッファのカーソル位置に文字列some textが 挿入される。

コマンドyankは、コピーしてテキストを複製するためにも使える。 コピーしたテキストはバッファからは切り取られずに、 そのコピーがキルリングに置かれ、ヤンクすると挿入できる。

キルリングからテキストを取り出すには3つの関数が使われる。 C-yにバインドされたyankM-yにバインドされた yank-pop、 これら2つの関数が使うrotate-yank-pointerである。

これらの関数は、変数kill-ring-yank-pointerを介してキルリングを参照する。 関数yankyank-popの挿入を行うコードはつぎのとおりである。

(insert (car kill-ring-yank-pointer))

yankyank-popがどのように動作するかを理解するには、 まず、変数kill-ring-yank-pointerと関数rotate-yank-pointerを 説明しておく必要がある。


Node:kill-ring-yank-pointer, Next:, Previous:Kill Ring Overview, Up:Yanking

変数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-ringkill-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自体のコードは難しくはないが、 こまごましたことを含む。 この関数と、さらに簡単な関数yankyank-popは、付録で説明する。 See Kill Ring


Node:yank nthcdr Exercises, Previous:kill-ring-yank-pointer, Up:Yanking

yanknthcdrの演習問題


Node:Loops & Recursion, Next:, Previous:Yanking, Up:Top

ループと再帰

Emacs Lispには、式や一連の式を繰り返し評価する主要な方法が2つあり、 whileループを使う方法と再帰(recursion)を使う方法である。

繰り返しはとても重要である。 たとえば、4つの文だけ先へ進むには、 1つの文のみ先へ進むだけのプログラムを書き、それを4回繰り返せばよい。 人間は繰り返し回数をまちがえたり処理を誤ったりするが、 コンピュータが飽きたり疲れたりすることはないので、 繰り返し動作が有害な結果を生むことはない。


Node:while, Next:, Previous:Loops & Recursion, Up:Loops & Recursion

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ループの本体の式を評価した効果だけのためである。 これは意味のあることである。 ほしいのは単なる繰り返し動作ではなく、 ループの式を繰り返し評価したときの効果がほしいのである。


Node:Loop Example, Next:, Previous:while, Up:while

whileループとリスト

whileループを制御する一般的な方法は、 リストに要素があるかどうかを検査することである。 要素があればループを繰り返すが、要素がなければ繰り返しを終える。 これは重要な技法なので、例示のために短い例を作ることにする。

リストに要素があるかどうかを検査する簡単な方法は、 リストを評価することである。 要素がなければ、空リストであるから空リスト()が返され、 これは偽を意味するnilの同義語である。 一方、リストに要素があれば、評価するとこれらの要素を返す。 Lispインタープリタは、nil以外の値を真と解釈するので、 要素を返すリストはwhileループの検査では真になる。

たとえば、つぎのsetq式を評価すれば、 変数empty-listnilを設定できる。

(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行に表示する関数で使える。


Node:print-elements-of-list, Next:, Previous:Loop Example, Up:while

例:print-elements-of-list

関数print-elements-of-listはリストを用いたwhileループの例である。

この関数は、複数行出力する。 エコー領域は1行分のみなので、これまでの関数の動作例を示すようには Infoの中で評価して動作を示すことができない。 かわりに、式をバッファ*scratch*にコピーしてから、 そのバッファで評価する必要がある。 式をコピーするには、 リージョンの始めをC-<SPC>set-mark-command)で マークし、カーソルをリージョンの終わりに移動してから、 M-wcopy-region-as-kill)を使ってリージョンをコピーする。 つぎに、バッファ*scratch*にて、 C-yyank)とタイプすればその式を取り出せる。

バッファ*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が表示される。


Node:Incrementing Loop, Next:, Previous:print-elements-of-list, Up:while

増加カウンタによるループ

終了すべきときに止まらないループは無意味である。 ループをリストで制御する以外に、ループを止める一般的な方法は、 必要回数の繰り返しを完了したら偽を返すような第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に設定する。


Node:Incrementing Example, Next:, Previous:Incrementing Loop, Up:Incrementing Loop

増加カウンタの例

浜辺で遊んでいるときに、つぎに示すように、 最初の行には小石を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つの数の加算処理を繰り返し繰り返し実行するのである。 より複雑なループでは、各段階の動作は単純ではないかもしれないが、 すべてをいっきに行うよりは簡単である。


Node:Inc Example parts, Next:, Previous:Incrementing Example, Up:Incrementing Loop

関数定義の各部分

以上の分析により、関数定義の骨格がわかる。 まず、小石の総数を保持する変数totalが必要である。 これは、関数が返す値である。

つぎに、関数には引数が必要である。 この引数は三角形の行数である。 これをnumber-of-rowsとしよう。

最後に、カウンタとして使う変数が必要である。 この変数をcounterと命名してもよいが、 より適したrow-numberを使おう。 カウンタが数えるのは行数であり、 プログラムではできる限りわかりやすい名前を使うべきだからである。

Lispインタープリタが関数内の式の評価を始めるときには、 totalには何も加算していないので、 totalの値は0になっているべきである。 続いて、関数では、1行目の小石の個数を総和に加算し、 2行目の小石の個数を総和に加算し、3行目の小石の個数を総和に加算し、 ということを、加算すべき行がなくなるまで行う。

totalrow-numberのどちらも、関数の内部だけで使うので、 letでローカル変数として宣言し初期値を与える。 明らかに、totalの初期値は0である。 また、第1行から始めるので、row-numberの初期値は1である。 つまり、let文はつぎのようになる。

  (let ((total 0)
        (row-number 1))
    本体...)

内部変数を宣言しそれらに初期値を束縛したら、whileループを始められる。 判定条件の式は、row-numbernumber-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))


Node:Inc Example altogether, Previous:Inc Example parts, Up:Incrementing Loop

関数定義をまとめる

関数定義の各部分を作ったので、それらを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が返す値となる。 しかし、whilelet式の最後の要素だったとすると、 関数はつねに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である。


Node:Decrementing Loop, Previous:Incrementing Loop, Up:while

減少カウンタによるループ

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減少


Node:Decrementing Example, Next:, Previous:Decrementing Loop, Up:Decrementing Loop

減少カウンタの例

減少カウンタによるループの例として、カウンタを0まで減少させるように 関数triangleを書き換える。

これは、この関数の前版の逆である。 つまり、3行の三角形を作るために必要な小石の個数は、 3行目の小石の個数3をそのまえの個数2に加え、2つの行の総数を そのまえの個数1に加えて計算する。

同様に、7行の三角形の小石の個数は、 7行目の小石の個数7をまえの行の個数6に加え、2つの行の総数を それらのまえの行の個数5に加え、と繰り返して計算する。 まえの例と同様に、各加算では2つの数、すでに加算した行の総数と、 加算すべき行の小石の個数を扱う。 2つの数を加える処理を、加えるべき小石がなくなるまで繰り返す。

始めの小石の個数はわかっている。 最後の行の小石の個数は、その行の番号に等しい。 7行の三角形の場合、最後の行の小石の個数は7である。 同様に、1つまえの行の小石の個数もわかっていて、行の番号より1つ小さい。


Node:Dec Example parts, Next:, Previous:Decrementing Example, Up:Decrementing Loop

関数の各部分

3つの変数が必要である。 三角形の行の数、行の小石の個数、求めたい小石の総数である。 これらの変数をそれぞれ、number-of-rowsnumber-of-pebbles-in-rowtotalとしよう。

totalnumber-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-rowtotalに加算したあと、 ループのつぎの繰り返しでは、まえの行を総和に加算するので、 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)


Node:Dec Example altogether, Previous:Dec Example parts, Up:Decrementing Loop

関数定義をまとめる

以上の式をまとめると、つぎのような関数定義を得る。

;;; 最初の減算版
(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つの部分から成る。

  1. 必要回数だけループを繰り返したら偽を返す判定条件。
  2. 繰り返し評価したあとに望みの値を返す式。
  3. 必要回数だけループを繰り返したら判定条件が偽を返すように、 判定条件で使う値を変更する式。


Node:Recursion, Next:, Previous:while, Up:Loops & Recursion

再 帰

再帰的関数では、自分自身の評価を指示するコードを含む。 関数が自分自身を評価するとき、自分自身の評価を指示するコードを 再度みつけるので、関数は自分自身を再度評価する...を繰り返す。 再帰的関数は、停止条件が与えられない限り、 自身を評価することを永久に続ける。

典型的な再帰的関数には、3つの部分から成る条件式が含まれる。

  1. 関数を再度呼び出すかどうかを決定する判定条件。 これを再帰条件(do-again-test)と呼ぶ。
  2. 関数名。
  3. 必要回数だけ繰り返したあとに条件式を偽にするような式。 これを次段式(next-step-expression)と呼ぶ。

再帰的関数は、他の種類の関数よりもとても簡単である。 もちろん、これを使い始めたばかりの人には、 理解できないほど不可思議に単純に見える。 自転車に乗るのと同じように、再帰的関数定義を読むには、 最初は難しくてもしだいに簡単に思えるようになるコツが必要である。

再帰的関数の雛型はつぎのようになる。

(defun 再帰的関数名 (引数リスト)
  "説明文..."
  本体...
  (if 再帰条件
    (再帰的関数名
         次段式)))

再帰的関数を評価するたびに、引数には次段式の値が束縛され、 その値を再帰条件で使う。 関数をそれ以上繰り返さない場合には、再帰条件が偽になるように次段式を作る。

再帰条件が偽になると繰り返しを停止するので、 再帰条件を停止条件(stop condition)と呼ぶこともある。


Node:Recursion with list, Next:, Previous:Recursion, Up: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回目なので)もとのリストのCDRCDRを 引数として自身を呼び出す。

関数が自身を呼び出すごとに、もとのリストを短くしたものを引数に渡す。 最終的に、空リストで自身を呼び出す。 関数printは空リストをnilと表示する。 つぎに、条件式ではlistの値を調べる。 listの値はnilなので、if式の判定条件は偽になり、 真の場合の動作は評価されない。 そして、関数全体としてはnilを返す。 そのため、この関数を評価するとnilが2つ表示されるのである。

バッファ*scratch*(print-elements-recursively animals)を 評価するとつぎのようになる。

giraffe

gazelle

lion

tiger

nil

nil

(最初のnilは空リストの値を表示したもので、 2番目のnilは関数全体としての戻り値である。)


Node:Recursive triangle function, Next:, Previous:Recursion with list, Up:Recursion

カウンタの代用としての再帰

前節で説明した関数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 もっとも内側の式を評価する。
もっとも内側の式は(1- number)であり、 Emacsはnumberの値を2から1へ減らす。
手順2 関数triangle-recursivelyを評価する。
関数の中にそれ自身があるかどうかは関係ない。 Emacsは手順1の結果を引数として使って、 関数triangle-recursivelyを呼び出す。

この場合、Emacsは引数1でtriangle-recursivelyを評価する。 つまり、triangle-recursivelyを評価すると1が返される。

手順3 numberの値を評価する。
変数numberは、+で始まるリストの2番目の要素であり、 その値は2である。
手順4 +式の評価。
+式は2つの引数、 numberの評価結果(手順3)と、triangle-recursivelyの評価結果 (手順2)を受け取る。

加算の結果は、2足す1で、数3が返される。 これは正しい値である。 2行の三角形には3個の小石がある。


Node:Recursive Example arg of 3, Previous:Recursive triangle function, Up:Recursive triangle function

引数が3の場合

引数3でtriangle-recursivelyが呼ばれた場合を考えよう。

手順1 再帰条件の評価。
最初にif式が評価される。 これは再帰条件であり偽を返すから、if式の偽の場合の動作が評価される (この例では、再帰条件が偽の場合に自身を呼び出すのであり、 真の場合には呼ばない)。
手順2 偽の場合の動作のもっとも内側の式を評価する。
偽の場合の動作のもっとも内側の式が評価され、3から2に減る。 これは次段式である。
手順3 関数triangle-recursivelyを評価する。
関数triangle-recursivelyに数2が渡される。

Emacsが、引数2でtriangle-recursivelyを評価するとどうなるかは すでに知っている。 上で述べたような動作順序のあとで、値3が返される。 ここでもそのようになる。

手順4 加算の評価。
3が加算の引数として渡され、関数呼び出しの結果の数3に加算される。

関数全体として返す値は、6になる。

これで、引数3でtriangle-recursivelyを呼ぶとどうなるかがわかった。 引数4で呼んだ場合にどうなるかも明らかであろう。

再帰呼び出しにおいて、
(triangle-recursively (1- 4))

の評価結果は、つぎの式の評価結果であり、

(triangle-recursively 3)

これは6であり、この値が3行目で4に加えられる。

関数全体として返す値は10である。

triangle-recursivelyを評価するたびに、 引数が小さくなりすぎて評価できなくなるまで、より小さな引数で自身を評価する。


Node:Recursion with cond, Previous:Recursive triangle function, Up:Recursion

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)))を 評価する。


Node:Looping exercise, Previous:Recursion, Up:Loops & Recursion

ループの演習問題


Node:Regexp Search, Next:, Previous:Loops & Recursion, Up:Top

正規表現の探索

正規表現の探索は、GNU Emacsでは非常に多用されている。 2つの関数forward-sentenceforward-paragraphは、 これらの探索のよい例である。

正規表現の探索に ついては、Regexp SearchRegular Expressionsに記述されている。 本章を執筆するにあたり、読者はこれらに関して少なくともある程度の知識を 持っていると仮定した。 主要な点は、正規表現により、文字列の字面どおりの探索に加えて、 パターンの探索もできることである。 たとえば、forward-sentenceのコードは、文末を表す可能性のある 文字のパターンを探索し、そこへポイントを移動する。

関数forward-sentenceのコードを説明するまえに、 文末を表すパターンとはどんなものであるかを考えることも価値がある。 このパターンについては次節で説明する。 続いて、正規表現の探索関数re-search-forwardを説明する。 さらに、関数forward-sentenceを説明する。 そして、本章の最後の節では、関数forward-paragraphを説明する。 forward-paragraphは複雑な関数であり、新たな機能もいくつか紹介する。


Node:sentence-end, Next:, Previous:Regexp Search, Up:Regexp Search

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
     => "[.?!][]\"')}]*\\($\\|     \\|  \\)[
]*"


Node:re-search-forward, Next:, Previous:sentence-end, Up:Regexp Search

関数re-search-forward

関数re-search-forwardは、関数search-forwardにとてもよく似ている (See search-forward)。

re-search-forwardは正規表現を探索する。 探索に成功すると、みつかった文字列の最後の文字の直後にポイントを置く。 逆向きの探索の場合には、みつかった文字列の最初の文字の直前にポイントを置く。 真値としてtを返すようにre-search-forwardに指示できる (したがって、ポイントの移動は「副作用」である)。

search-forwardのように、関数re-search-forwardは4つの引数を取る。

  1. 第1引数は、関数が探索する正規表現である。 正規表現は引用符のあいだの文字列である。
  2. 省略できる第2引数は、関数が探索する範囲を制限する。 限界はバッファの位置で指定する。
  3. 省略できる第3引数は、失敗した場合の関数の動作を指定する。 第3引数にnilを指定すると、探索に失敗した場合、 関数はエラーを通知(し、メッセージを表示)する。 それ以外の値を指定すると、探索に失敗した場合はnilを返し、 探索に成功するとtを返す。
  4. 省略できる第4引数は、繰り返し回数である。 負の繰り返し回数は、re-search-forwardに逆向き探索を指示する。

re-search-forwardの雛型はつぎのとおりである。

(re-search-forward "正規表現"
                探索範囲
                探索失敗時の動作
                繰り返し回数)

第2引数、第3引数、第4引数は省略できる。 しかし、最後の2つの引数のいずれか、あるいは、両者に値を渡したい場合には、 そのまえにある引数すべてに値を渡す必要がある。 さもないと、Lispインタープリタはどの引数に値を渡すのかを誤解することになる。

関数forward-sentenceでは、 正規表現は変数sentence-endの値であり、つぎのとおりである。

"[.?!][]\"')}]*\\($\\|  \\|  \\)[
]*"

探索の範囲は、(文は段落を越えることはないので)段落の末尾までである。 探索に失敗すると、関数はnilを返す。 繰り返し回数は、関数forward-sentenceの引数で与える。


Node:forward-sentence, Next:, Previous:re-search-forward, Up:Regexp Search

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に値が束縛されていればそのままにするが、 argnilが束縛されているときにはargの値を1にする。


Node:fwd-sentence while loops, Next:, Previous:forward-sentence, Up:forward-sentence

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でこの種の操作を行う一般的な方法である。


Node:fwd-sentence re-search, Previous:fwd-sentence while loops, Up:forward-sentence

正規表現の探索

関数re-search-forwardは文末を探す、 つまり、正規表現sentence-endで定義されたパターンを探す。 パターンがみつかれば、つまり、文末がみつかれば、 関数re-search-forwardはつぎの2つのことを行う。

  1. 関数re-search-forwardは、副作用を及ぼす。 つまり、みつけた文字列の直後にポイントを移動する。
  2. 関数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はパターンの例題として簡便である。 読者もこのパターンをしばしば利用するであろう。


Node:forward-paragraph, Next:, Previous:forward-sentence, Up:Regexp Search

forward-paragraph:関数の宝庫

関数forward-paragraphは、段落の末尾にポイントを進める。 普通はM-}にバインドされており、 let*match-beginninglooking-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)。 この関数の馴染みのある部分はこれで終わりである。


Node:fwd-para let, Next:, Previous:forward-paragraph, Up:forward-paragraph

let*

関数forward-paragraphのつぎの行はlet*式で始まる。 これはこれまでに見てきた式とは種類が異なる。 シンボルはlet*であり、letではない。

スペシャルフォームlet*letに似ているが、 Emacsは1つずつ順に各変数を設定し、変数リストのうしろの変数では、 Emacsがすでに設定した変数リストのまえの部分の変数の値を利用してもよいところが 異なる。

この関数のlet*式では、Emacsは2つの変数、 fill-prefix-regexpparagraph-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-regexpnilに束縛される。

fill-prefix
この変数を評価すると、もしあれば、詰め込み接頭辞の値が返される。 詰め込み接頭辞がなければ、この変数はnilを返す。
(not (equal fill-prefix "")
この式は、既存の詰め込み接頭辞が空文字列、つまり、文字をまったく含まない 文字列かどうかを調べる。 空文字列は、意味のある詰め込み接頭辞ではない。
(not paragraph-ignore-fill-prefix)
この式は、変数paragraph-ignore-fill-prefixtなどの真値を設定してオンになっていると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-regexpnilであるか それ以外の値であるかに依存している。

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*の本体の始めの部分は、関数に負の引数を与えた場合の処理であり、 逆向きに戻す。 本節では、割愛する。


Node:fwd-para while, Next:, Previous:fwd-para let, Up:forward-paragraph

先へ進める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式で、ポイントを段落の末尾に実際に移動する。


Node:fwd-para between paragraphs, Next:, Previous:fwd-para while, Up:forward-paragraph

段落のあいだ

まず、内側のwhileループを説明しよう。 このループは、ポイントが段落のあいだにある場合を扱う。 3つの新たな関数、prog1eobplooking-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行目の先頭にポイントが移動するのである。


Node:fwd-para within paragraph, Next:, Previous:fwd-para between paragraphs, Up:forward-paragraph

段落の中

外側のwhileループのつぎの式はif式である。 変数fill-prefix-regexpnil以外の値を持っている場合には、 Lispインタープリタはifの真の場合の動作を評価し、 fill-prefix-regexpの値がnilの場合、 つまり、詰め込み接頭辞がない場合には偽の場合の動作を評価する。


Node:fwd-para no fill prefix, Next:, Previous:fwd-para within paragraph, Up:forward-paragraph

詰め込み接頭辞なし

詰め込み接頭辞がない場合のコードを見るほうが簡単である。 このコードは、さらに内側に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は、 直前の正規表現の中の括弧表現にポイントを置く。 これは便利な機能である。)


Node:fwd-para with fill prefix, Next:, Previous:fwd-para no fill prefix, Up:forward-paragraph

詰め込み接頭辞あり

説明したばかりの内側のif式は、詰め込み接頭辞の有無を調べる if式の偽の場合の動作である。 詰め込み接頭辞がある場合には、このif式の真の場合の動作が評価される。 それはつぎのとおりである。

(while (and (not (eobp))
            (not (looking-at paragraph-separate))
            (looking-at fill-prefix-regexp))
  (forward-line 1))

この式は、つぎの3つの条件が真である限り、ポイントを1行進める。

  1. ポイントはバッファの最後にいない。
  2. ポイントに続くテキストは段落の区切りではない。
  3. 詰め込み接頭辞の正規表現に一致するパターンがポイントに続けてある。

このまえにある関数forward-paragraphで行の先頭にポイントが移動している ことを思い出さないと、最後の条件に惑わされるかもしれない。 つまり、テキストに詰め込み接頭辞がある場合、 関数looking-atはそれをみつけるのである。


Node:fwd-para summary, Previous:fwd-para with fill prefix, Up:forward-paragraph

まとめ

まとめると、関数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ファイルを作成できる。


Node:etags, Next:, Previous:forward-paragraph, Up:Regexp Search

専用タグファイルの作成方法

ソースを調べ廻る際の補助として、個人用のタグファイルを作成できる。 筆者のディレクトリ~/emacsには、137個の.elファイルがあり、 そのうちの17個をロードしている。 読者のディレクトリ~/emacsにも多数のファイルがある場合、 タグファイルを作っておけば望みの関数にジャンプでき、 grepなどのツールで関数名を探すより簡単である。

タグファイルを作成するには、Emacsのディストリビューションに含まれる プログラムetagsを使う。 普通、Emacsを作るときにetagsもコンパイルされてインストールされる (etagsはEmacs Lispの関数でもEmacsの一部でもない。 Cのプログラムである)。

タグファイルを作るには、タグファイルを作りたいディレクトリにまず移動する。 Emacsの中では、コマンドM-x cdで行うか、 そのディレクトリのファイルを訪問するか、 C-x ddired)でディレクトリを表示する。 続いて、

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の一部になる。


Node:Regexp Review, Next:, Previous:etags, Up:Regexp Search

復 習

説明した関数のうち、いくつかを簡素にまとめておく。

while
第1引数が真である限り、式の本体を繰り返し評価する。 そして、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つの引数を取る。

  1. 探索するパターンを指定する正規表現。
  2. 探索範囲を制限する。 省略できる。
  3. 探索に失敗した場合にnilを返すかエラーメッセージを返すかを指定する。 省略できる。
  4. 探索を何回行うかを指定する。 負の場合には、逆向きの探索をする。 省略できる。

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


Node:re-search Exercises, Previous:Regexp Review, Up:Regexp Search

re-search-forwardの演習問題


Node:Counting Words, Next:, Previous:Regexp Search, Up:Top

数え上げ:繰り返しと正規表現

繰り返しと正規表現の探索は、Emacs Lispを書くときによく使う強力な道具である。 本章では、whileループと再帰を用いた単語を数えるコマンドの 作成をとおして、正規表現の探索の使用例を示す。


Node:Why Count Words, Next:, Previous:Counting Words, Up:Counting Words

Emacsの標準ディストリビューションには、 リージョン内の行数を数える関数が含まれている。 しかし、単語を数える関数はない。

ある種の文書の作成過程では、単語数を知る必要がある。 たとえば、エッセイは800語までとか、 小説を執筆するときには1日に1000語は書くことにするとかである。 Emacsに単語を数えるコマンドがないのは筆者には奇妙に思える。 たぶん、単語数を数える必要のないコードやドキュメントを書くのに Emacsを使っているのであろう。 あるいは、オペレーティングシステムの単語を数えるコマンドwcを 使っているのであろう。 あるいは、出版社の慣習にしたがって、文書の文字数を5で割って 単語数を計算しているのであろう。


Node:count-words-region, Next:, Previous:Why Count Words, Up:Counting Words

関数count-words-region

単語を数えるコマンドは、行、段落、リージョン、あるいは、 バッファのなかの単語を数える。 どの範囲で数えるべきであろう?  バッファ全体で単語数を数えるようにコマンドを設計することもできるが、 Emacsの習慣では柔軟性を重んじる。 バッファ全体ではなく、ある部分の単語数を数えたい場合もある。 したがって、リージョンの中の単語数を数えるようにコマンドを 設計するほうが合理的であろう。 コマンドcount-words-regionさえあれば、 必要ならば、C-x hmark-whole-buffer)で バッファ全体をリージョンとして単語を数えられる。

明らかに、単語を数えるのは繰り返し動作である。 リージョンの先頭から始めて、最初の語を数え、2番目の語を数え、3番目の語を数え というように、リージョンの最後に達するまで繰り返す。 つまり、単語の数え上げは、再帰やwhileループに完璧に適しているのである。

まず、whileループで単語を数えるコマンドを実装してから、 再帰でも書いてみる。 コマンドは、当然、対話的にする。

対話的関数の雛型はつぎのとおりである。

(defun 関数名 (引数リスト)
  "説明文..."
  (interactive-expression...)
  本体...)

これらの項目を埋めればよいのである。

関数名は、十分説明的で既存の名前count-lines-regionに 似ているべきである。 こうすると、名前を覚えやすい。 count-words-regionがよいであろう。

関数はリージョン内の単語を数える。 つまり、引数リストには、リージョンの先頭の位置と最後の位置に束縛される シンボルが含まれる必要がある。 これらの2つの位置を、それぞれ、beginningendと呼ぶことにする。 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*")

wWのまえに、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))))))

関数はこのとおりに動作するが、正しくない場合もある。


Node:Whitespace Bug, Previous:count-words-region, Up: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-gkeyboard-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))))))


Node:recursive-count-words, Next:, Previous:count-words-region, Up:Counting Words

再帰による単語の数え上げ

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を返すべきである。

しかし、再帰呼び出しを考えるまえに、次段式を考える必要がある。 どうすべきだろう?  興味深いことに、次段式は再帰条件の探索部分である。

再帰条件にtnilを返すことに加えて、 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))))))


Node:Counting Exercise, Previous:recursive-count-words, Up:Counting Words

演習問題:句読点の数え上げ

whileを使って、リージョン内の句読点、ピリオド、カンマ、セミコロン、 コロン、感嘆符、疑問符を数える関数を書いてみよ。 また、再帰を使って書いてみよ。


Node:Words in a defun, Next:, Previous:Counting Words, Up:Top

defun内の単語の数え上げ

つぎの目標は、関数定義内の語数を数えることである。 明らかに、count-words-regionの変形を使えばできる。 See Counting Words。 1つの定義内の単語を数えるだけならば、 定義をコマンドC-M-hmark-defun)でマークして、 count-words-regionを呼べばよい。

しかし、より野心的でありたい。 Emacsソース内の各定義ごとに単語やシンボルを数え、それぞれの数ごとに いくつの関数があるかを表すグラフを書きたい。 40から49個の単語やシンボルを含む関数はいくつ、 50から59個の単語やシンボルを含む関数はいくつなどを表示したいのである。 筆者は、典型的な関数定義の長さに興味を持っており、この関数でそれがわかる。


Node:Divide and Conquer, Next:, Previous:Words in a defun, Up:Words in a defun

一言でいえば、目標はヒストグラムを作ることである。 多数の小さな部分に分けて一度に1つずつ扱えば、恐れることはない。 どのような手順を踏めばよいか考えよう。

これはとても手応えのあることである。 しかし、順を追って進めれば、困難ではない。


Node:Words and Symbols, Next:, Previous:Divide and Conquer, Up:Words in a defun

何を数えるか?

関数定義内の単語を数えることを考えた場合、 最初の疑問は何を数えるのかということである。 Lispの関数定義に関して「単語」といえば、ほとんどの場合、 「シンボル」のことである。 たとえば、つぎの関数multiply-by-sevenには、 defunmultiply-by-sevennumber*7の5つのシンボルがある。 さらに、4つの単語、MultiplyNUMBERbysevenを 含んだ説明文字列がある。 シンボルnumberは繰り返し使われているので、 定義には全部で10個の単語とシンボルが含まれる。

(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))

しかし、multiply-by-sevenの定義にC-M-hmark-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つの節で扱おう。


Node:Syntax, Next:, Previous:Words and Symbols, Up:Words in a defun

単語やシンボルを構成するものは何か?

Emacsは、それぞれの文字をそれぞれが属する シンタックスカテゴリ(syntax categories)に応じて扱う。 たとえば、正規表現\\w+は、単語構成文字の1個以上の繰り返しを 意味するパターンである。 単語構成文字は、1つのシンタックスカテゴリである。 他のシンタックスカテゴリには、ピリオドやカンマなどの句読点文字のクラスや、 空白文字やタブ文字などの空白文字クラスがある (より詳しくは、SyntaxSyntax Tablesを参照)。

シンタックステーブルは、どの文字がどのカテゴリに属するかを指定する。 普通、ハイフンは「単語構成文字」ではない。 かわりに、「シンボルの一部ではあるが単語の一部ではないクラス」に指定される。 つまり、関数count-words-regionは、ハイフンを単語のあいだの 空白と同様に扱い、そのためcount-words-regionmultiply-by-sevenを3語と数えるのである。

Emacsにmultiply-by-sevenを1つのシンボルとして数えさせるには、 2つの方法がある。 シンタックステーブルを変更するか、正規表現を変更するかである。

Emacsが各モードごとに保持するシンタックステーブルを変更して、 ハイフンを単語構成文字として再定義することもできる。 この方法でもわれわれの目的は達せられるが、 ハイフンはシンボルの一部になるもっとも一般的な文字であるが、 典型的な単語構成文字ではない。 このような文字は他にもある。

かわりに、count-words-regionの定義の中の正規表現を シンボルを含むように再定義することである。 この方法は明確ではあるが、技巧を要する。

最初の部分は簡単で、パターンは「単語やシンボルを構成する少なくとも1文字」 である。 つまり、つぎのようになる。

\\(\\w\\|\\s_\\)+

\\(は、\\|で区切られた\\wと代替の\\s_を 含むグループ構成の開始部分である。 \\wは任意の単語構成文字に一致し、 \\s_は単語構成文字ではないがシンボル名の一部に 成りえる任意の文字に一致する。 グループに続く+は、単語やシンボルを構成する文字が少なくとも1つ あることを意味する。

しかし、正規表現の第二の部分は、設計がより困難である。 最初の部分に続けて、「単語やシンボルを構成しない文字が0個以上続く」と 指定したいのである。 まず、筆者は、つぎのような定義を考えた。

\\(\\W\\|\\S_\\)*"

大文字のWSは、単語やシンボルを構成しない文字に一致する。 残念ながら、この式は、単語を構成しない任意の文字かシンボルを構成しない 任意の文字に一致する。 つまり、任意の文字に一致するのである!

筆者が試したリージョンでは、単語やシンボルには空白文字 (空白、タブ、改行)が続いていることに気づいた。 そこで、単語やシンボルを構成する文字の1個以上の繰り返しパターンのあとに 1個以上の空白文字が続くパターンを試してみた。 これも失敗であった。 単語やシンボルはしばしば空白で区切られるが、実際のコードでは、 シンボルのあとには括弧が、単語のあとには句読点が続く。 結局、単語やシンボルを構成する文字に続いて空白文字以外の文字が0個以上続き、 さらに、空白文字が0個以上続くパターンにした。

全体の正規表現はつぎのとおりである。

"\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*"


Node:count-words-in-defun, Next:, Previous:Syntax, Up:Words in a defun

関数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-defuncount-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*などの他のバッファに 切り替えていた。

ファイルを探すという動作は、まだ説明していない新しい動作である。


Node:Find a File, Next:, Previous:Several defuns, Up:Words in a defun

ファイルを探す

Emacsでファイルを探すには、コマンドC-x C-ffind-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-noselectswitch-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-noselectset-bufferを使う。


Node:lengths-list-file, Next:, Previous:Find a File, Up:Words in a defun

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

ファイルの最後にある定義の長さがリストの先頭にあることに注意してほしい。


Node:Several files, Next:, Previous:lengths-list-file, Up:Words in a defun

別のファイルの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であるが、 次節で説明する。


Node:append, Previous:Several files, Up:Several files

関数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)


Node:Several files recursively, Next:, Previous:Several files, Up:Words in a defun

別のファイルの再帰による単語の数え上げ

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-fileslengths-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は、望みの結果を出している。

つぎの段階は、グラフ表示するためにリスト内のデータを準備することである。


Node:Prepare the data, Previous:Several files recursively, Up:Words in a defun

グラフ表示用データの準備

関数recursive-lengths-list-many-filesは、個数のリストを返す。 各個数は、関数定義の長さである。 このデータを、グラフ作成に適した数のリストに変換したいのである。 新しいリストでは、単語やシンボルが10個未満の関数定義はいくつ、 10から19個のものはいくつ、20から29個のものはいくつ、などとしたい。

つまり、関数recursive-lengths-list-many-filesが作成した 長さのリスト全体を処理して、長さの範囲ごとに数を数えて、 これらの数から成るリストを作りたいのである。

これまでの知識をもとにすれば、 長さのリストの「CDRs」を辿りながら、各要素を調べ、 どの長さの範囲に含まれるかを決めて、その範囲のカウンタを増やす関数を 書くことは難しくないことがわかる。

しかし、そのような関数を書き始めるまえに、 長さのリストを最小数から最大数の順にソートした場合の利点を考えてみよう。 まず、ソートしてあると、2つの並んだ数は同じ範囲か隣り合う範囲にあるので、 各範囲の数を数えるのが簡単になる。 第二に、ソートされたリストを調べれば、最大数と最小数がわかるので、 最大の範囲と最小の範囲を決定できる。


Node:Sorting, Next:, Previous:Prepare the data, Up:Prepare the data

リストのソート(整列)

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引数はクオートしない。)


Node:Files List, Previous:Sorting, Up:Prepare the data

ファイルのリストの作成

関数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は「破壊的」である。 つまり、渡されたリストそのものを変更する。 非破壊的な関数carcdrとは対照的である。 ここでは、もとの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-lengthsnilでない限り、 正しく動作する。 しかし、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つある。


Node:Readying a Graph, Next:, Previous:Words in a defun, Up:Top

グラフの準備

われわれの目標は、Emacs Lispのソースコードにあるさまざまな長さの 関数定義の個数を表示したグラフを作ることである。

実際問題として、グラフを作る場合にはgnuplotなどの プログラムを使うであろう (gnuplotはGNU Emacsとうまく組み合わせることができる)。 しかし、ここでは、ゼロからグラフを描くプログラムを作り、 その過程をとおして、すでに学んだことを復習し、より多くを学ぼう。

本章では、単純なグラフを描く関数をまず書いてみる。 この定義はプロトタイプ(prototype)であり、 素早く書いた関数であるが、グラフ作成という未踏領域の探検を可能にしてくれる。 ドラゴンを発見するか単なる伝説であることを知るであろう。 地形を把握できたら、自動的に軸のラベルを描くように関数を拡張する。


Node:Columns of a graph, Next:, Previous:Readying a Graph, Up:Readying a Graph

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-forwardinsert-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-blankgraph-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と呼ぶことにする。


Node:graph-body-print, Next:, Previous:Columns of a graph, Up:Readying a Graph

関数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))を使う。 リストのCARcolumn-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を試そう。

  1. graph-symbolgraph-blankcolumn-of-graphgraph-body-printをインストールする。
  2. つぎの式をコピーする。
    (graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3))
    
  3. バッファ*scratch*に切り替え、 グラフを描き始める位置にカーソルを置く。
  4. M-<ESC>eval-expression)とタイプする。
  5. ミニバッファにC-yyank)でgraph-body-print式をヤンクする。
  6. <RET>を押してgraph-body-print式を評価する。

Emacsはつぎのようなグラフを描く。

                    *
                *   **
                *  ****
               *** ****
              ********* *
             ************
            *************


Node:recursive-graph-body-print, Next:, Previous:graph-body-print, Up:Readying a Graph

関数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-printrecursive-graph-body-printの いずれも、グラフの本体を作る。


Node:Printed Axes, Next:, Previous:recursive-graph-body-print, Up:Readying a Graph

軸表示の必要性

グラフを読み取るためには、グラフの軸が必要である。 一度だけならば、Emacsのピクチャーモードを使って、 手で軸を描くのも合理的であろう。 しかし、グラフ描画関数は何回も使われる。

このため、基本の関数print-graph-bodyを拡張して、 水平軸と垂直軸のラベルを自動的に描くようにした。 ラベル表示関数には新しいことがらはないので、のちほど付録で説明する。 See Full Graph


Node:Line Graph Exercise, Previous:Printed Axes, Up:Readying a Graph

演習問題

折線グラフを描くグラフ表示関数を書いてみよ。


Node:Emacs Initialization, Next:, Previous:Readying a Graph, Up:Top

個人用ファイル.emacs

「Emacsを好きになるにはEmacsが好きである必要はない」。 この矛盾するような言葉は、GNU Emacsの秘密である。 「箱から取り出した」ままのEmacsは、汎用のツールである。 Emacsを使うほとんどの人は、満足のいくようにカスタマイズする。

GNU Emacsは、ほとんどEmacs Lispで書かれている。 つまり、Emacs Lispの式を書けば、Emacsを修正したり拡張できるのである。


Node:Default Configuration, Next:, Previous:Emacs Initialization, Up:Emacs Initialization

Emacsのデフォルトの設定に満足する人もいる。 Emacsは、Cのファイルを編集するとCモードで、 Fortranのファイルを編集するとFortanモードで、 普通のファイルを編集すると基本(Fundamental)モードで始まる。 誰がEmacsを使おうとしているのかわからない場合には、 これらはすべて意味があることである。 普通のファイルで何をしたいか予測できる人がいるだろうか?  Cのコードを編集するときにはCモードが正しいデフォルトであるように、 基本モードはそのようなファイルに対する正しいデフォルトである。 しかし、読者自身がEmacsを使う場合には、 Emacsをカスタマイズすることに意味がある。

たとえば、筆者は特定のファイル以外では基本モードを好み、Textモードを好む。 そのために、Emacsをカスタマイズして、筆者に適するようにする。

~/.emacsファイルを書くことで、 Emacsをカスタマイズしたり拡張できる。 これは、個人用の初期化ファイルであり、 その内容はEmacs Lispで書かれており、Emacsに何をすべきかを指示する。

本章では、簡単な~/.emacsファイルについて説明する。 より詳しくは、Init FileInit Fileを参照してほしい。


Node:Site-wide Init, Next:, Previous:Default Configuration, Up:Emacs Initialization

サイト全体の初期化ファイル

個人用の初期化ファイルに加えて、 Emacsはサイト全体のさまざまな初期化ファイルを自動的にロードする。 これらは個人用の~/.emacsファイルと同じ形式であるが、 誰もがロードするものである。

サイト全体の2つの初期化ファイル、site-load.elsite-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-prefixnilを設定する。 See Simple Extension)。

ディストリビューションに含まれるファイルINSTALLには、 ファイルsite-init.elsite-load.elに関する説明がある。

ファイルloadup.elstartup.elloaddefs.elは、 ファイルのロードを制御する。 これらのファイルは、Emacsディストリビューションのディレクトリlispに あり、見ておく価値がある。

ファイルloaddefs.elには、個人の.emacsファイルや サイト全体の初期化ファイルに何を書くべきかについて多くの有用な助言がある。


Node:edit-options, Next:, Previous:Site-wide Init, Up:Emacs Initialization

一回だけの作業用の変数の設定

筆者の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


Node:Beginning a .emacs File, Next:, Previous:edit-options, Up:Emacs Initialization

ファイル.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ファイルに、これらの注釈を含める必要はない。 筆者のファイルに入れておいたのは、モードヘルプや注釈の習慣を忘れやすかった からである。 ここに書いておけば、見れば思い出せる。


Node:Text and Auto-fill, Next:, Previous:Beginning a .emacs File, Up:Emacs Initialization

TextモードとAuto Fillモード

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-hookturn-on-auto-fillを追加する。 turn-on-auto-fillは、読者の予想どおり、プログラムの名前であり、 Auto Fillモードを有効にする。

Textモードを有効にするたびに、 EmacsはTextモードにフックしたコマンドを実行する。 つまり、Textモードを有効にするたびに、Auto Fillモードも有効にする。

まとめると、1行目は、ファイル名の拡張子や最初の空行以外の行や ローカル変数で指定されない限り、ファイルを編集するときは、 Textモードを有効にする指示である。

Textモードは、他のことも含めて、普通の文書作成に適した シンタックステーブルを設定する。 Textモードでは、Emacsは引用符を単語の一部として扱う。 しかし、ピリオドや空白は単語の一部ではない。 したがって、M-fit'sを飛び越せる。 一方、Cモードでは、M-fit'stの直後で止まる。

2行目は、Textモードを有効にしたらAuto Fillモードも有効にする指示である。 Auto Fillモードでは、Emacsは自動的に長すぎる行を分割し、つぎの行へ送る。 Emacsは、単語の途中ではなく、単語と単語のあいだで行を分ける。

Auto Fillモードを無効にすると、タイプしたとおりの行分けになる。 truncate-linesに設定した値に依存して、 タイプした単語が画面の右端から消えてしまうか、 見にくいが画面上に継続行として表示される。


Node:Mail Aliases, Next:, Previous:Text and Auto-fill, Up:Emacs Initialization

メールの別名

メモ書きも含めて、メールの別名を有効にする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を完全なアドレスに展開する。


Node:Indent Tabs Mode, Next:, Previous:Mail Aliases, Up:Emacs Initialization

Indent Tabsモード(タブによる字下げ)

デフォルトでは、リージョンを整形するときに、 Emacsは複数の空白のかわりにタブを挿入する (たとえば、コマンドindent-regionで一度に複数の行を字下げする場合)。 端末や普通の印刷ではタブは適しているのだが、 TeXはタブを無視するのでTeXやTexinfoでの字下げには適さない。

つぎの行は、Indent Tabsモードを無効にする。

;;; Prevent Extraneous Tabs
(setq-default indent-tabs-mode nil)

これまでのようにコマンドsetqではなく、 setq-defaultを使っていることに注意してほしい。 コマンドsetq-defaultは、 その変数にバッファ独自の値が設定されていないバッファでのみ値を設定する。


Node:Keybindings, Next:, Previous:Indent Tabs Mode, Up:Emacs Initialization

キーバインド例

つぎは、個人用のキーバインドである。

;;; Compare windows
(global-set-key "\C-cw" 'compare-windows)

compare-windowsは、カレントウィンドウとつぎのウィンドウの テキストを比較するちょっとしたコマンドである。 両者のウィンドウのポイント位置から始めて、一致する限り 両者のウィンドウのテキストを比較する。 筆者はこのコマンドをよく使う。

これは、すべてのモードに対して大局的にキーを設定する方法でもある。

コマンドはglobal-set-keyである。 これに続けてキーバインドを書く。 .emacsファイルでは、ここに記したようにキーバインドを書く。 \C-cは「control-c」のことで、 コントロールキーとcキーを同時に押し下げることを意味する。 wwキーを押し下げることを意味する。 キーバインドは二重引用符で囲む。 説明文章などでは、これを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を好むのである。


Node:Loading Files, Next:, Previous:Keybindings, Up:Emacs Initialization

ファイルのロード

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


Node:Autoload, Next:, Previous:Loading Files, Up:Emacs Initialization

オートロード

関数を収めたファイルをロードしたり関数定義を評価したりして関数を インストールするかわりに、関数を利用できるようにはするが、 最初に呼び出すまで実際にはインストールしないようにできる。 これをオートロード(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


Node:Simple Extension, Next:, Previous:Autoload, Up:Emacs Initialization

簡単な拡張: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 lview-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
  ...


Node:Keymaps, Next:, Previous:Simple Extension, Up:Emacs Initialization

キーマップ

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-@groupC-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.ellisp-mode.elのような、さまざまなモードのライブラリに 加えて、数多くのdefine-key式がある。

キーマップについて詳しくは、 Keymapsや See Key Bindings


Node:X11 Colors, Next:, Previous:Keymaps, Up:Emacs Initialization

X11の色指定

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

青のさまざまな濃淡は目にやさしく、画面のチラツキを目立たせない。


Node:V19 Miscellaneous, Next:, Previous:X11 Colors, Up:Emacs Initialization

第19版のその他

Emacs第19版のその他の雑多な設定の一部を説明しておく。


Node:Mode Line, Previous:V19 Miscellaneous, Up:Emacs Initialization

モード行の変更

最後に、筆者のお気に入りの機能を説明する。 モード行の変更である。

筆者はネットワークを介して作業することがあるので、 モード行の左端に表示される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である。 何も変わっていない。


Node:Debugging, Next:, Previous:Emacs Initialization, Up:Top

デバッグ

GNU Emacsには2つのデバッガ、debugedebugがある。 最初のものは、Emacs内部に組み込まれていていつでも使える。 2番目のものはEmacsに対する拡張であり、 第19版で標準ディストリビューションの一部になった。

どちらのデバッガもDebuggingに詳しく記載されている。 本章では、それぞれの短い例を示すことにする。


Node:debug, Next:, Previous:Debugging, Up: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-errornilを設定すればデバッガを無効にできる。

(setq debug-on-error nil)

debug-on-errortを設定し、つぎの式を評価する。

(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-eeval-last-sexp)を対話的に呼び出し、 これはtriangle-bugged式の評価につながった。 各行はLispインタープリタが、つぎに何を評価したのかを教えてくれる。

バッファの先頭から3行目はつぎのとおりである。

(setq number (1= number))

Emacsはこの式を評価しようとした。 そのために、先頭から2行目に示されたもっとも内側の式を評価しようとした。

(1= number)

これがエラーを起こした場所であり、 先頭の行にはつぎのように表示されている。

Signalling: (void-function 1=)

この誤りを修正して、関数定義を再評価し、再度試せばよい。

Infoで読んでいる場合には、 debug-on-errornilを設定してデバッガを無効にする。

(setq debug-on-error nil)


Node:debug-on-entry, Next:, Previous:debug, Up:Debugging

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を取り消す。)


Node:debug-on-quit, Next:, Previous:debug-on-entry, Up:Debugging

debug-on-quit(debug)

debug-on-errorを設定したりdebug-on-entryを呼んだりすることに 加えて、debugを起動するには別に2つの方法がある。

変数debug-on-quittに設定すると、 C-gkeyboard-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に詳しく記載されている。


Node:edebug, Next:, Previous:debug-on-quit, Up:Debugging

ソースレベルのデバッガ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-eeval-last-sexp)とタイプするか、 あるいは、定義の途中にカーソルを置いてC-M-xeval-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-eeval-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に記載されている。


Node:Debugging Exercises, Previous:edebug, Up:Debugging

デバッグの演習問題


Node:Conclusion, Next:, Previous:Debugging, Up:Top

結 論

これで、本書の終わりまできた。 値を設定する、自分のために簡単な.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-forwardindent-toinsert?\nが 含まれている。

関数insertを考えてみよう(これは、 Emacsでは、C-h fdescribe-function)に 続けて関数名をタイプすれば、insertについてさらに調べることができる。 関数の説明文を表示してくれる。 M-.にバインドされたfind-tagを使えばソースを見ることもできる (ここではこれはあまり役立たない。 この関数はLispではなくCで書いた基本操作関数である)。 最後に、リファレンスマニュアルにあるように、 iInfo-index)に続けて関数名をタイプして Infoのマニュアルを見るか、印刷したマニュアルの索引からinsertを探す。

?\nの意味も同様にして調べられる。 Info-index?\nを指定してみる。 これは役立たないことがわかるであろうが、諦めないでほしい。 ?を付けずに\nだけで探すと、マニュアルの関連する節に辿りつける (See Character Type?\nは改行を意味する)。

skip-chars-forwardindent-toの動作を予想することもできるし、 探すこともできる (関数describe-function自身はhelp.elにある。 長い関数の1つであるが、読みこなせる関数である。 この定義は、標準のコード文字を使わずにinteractive式を カスタマイズする例になる。 一時的なバッファの作成方法の例でもある)。

その他の興味深いファイルは、paragraphs.elloaddefs.elloadup.elである。 ファイルparagraphs.elには、長いものもあるが、 短くて理解しやすい関数がある。 ファイルloaddefs.elには、標準的な多数のオートロードや 多数のキーマップがある。 筆者もその一部を見ただけで、すべてを見たことはない。 loadup.elは、Emacsの標準的なものをロードするためのファイルである。 Emacsの構築方法に関してとても詳しく教えてくれる (構築に関して詳しくは、See Building Emacs)。

初歩は学べたはずである。 しかし、プログラミングの重要な側面にはふれなかった。 既知の関数sortを使う以外には、 情報をソートする方法については説明しなかった。 変数やリストを使うことを除いて、情報の保存方法については説明しなかった。 プログラムを作成するプログラムの書き方については説明しなかった。 これらは、別の話題である。

GNU Emacsを実用的に使うに十分なことは学べたと思う。 やっと、始めたばかりである。 これで入門を終える。


Node:the-the, Next:, Previous:Conclusion, Up:Top

関数the-the

文書を書いているときに、「you you」などと単語をだぶらせることがある。 筆者は、しばしば「the」をだぶって書いていることに気づいた。 そこで、重複した単語を検出する関数をthe-theと命名する。

第1段階としては、重複の検出にはつぎの正規表現が使える。

\\(\\w+[ \t\n]+\\)\\1

この正規表現は、単語構成文字の1個以上の繰り返しに続いて 空白文字やタブや改行が1個以上続くものに一致する。 しかし、最初の単語の終わりである改行は、2番目の単語の終わりである空白文字とは 異なるので、2行にわたる単語の重複は検出できない (正規表現についてより詳しくは、 Regexp SearchRegexpsRegular 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

上の関数定義の正規表現を別の正規表現に置き換えることもでき、 それぞれをこのテキストで試すとよい。


Node:Kill Ring, Next:, Previous:the-the, Up:Top

キルリングの扱い方

キルリングはリストであるが、 関数rotate-yank-pointerの働きでリングに変換されている。 コマンドyankyank-popは、 関数rotate-yank-pointerを使っている。 本付録では、関数rotate-yank-pointerとともに、 コマンドyankyank-popを説明する。


Node:rotate-yank-pointer, Next:, Previous:Kill Ring, Up:Kill Ring

関数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)は、キルリングにある要素の個数を返す。


Node:rotate-yk-ptr body, Previous:rotate-yank-pointer, Up: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」のような用語のほうが、はっきりと意味を表す。)


Node:rotate-yk-ptr else-part, Next:, Previous:rotate-yk-ptr body, Up:rotate-yk-ptr body

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を繰り返し取る。 CDRCDRCDR ...を取る。

つぎの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)


Node:Remainder Function, Next:, Previous:rotate-yk-ptr else-part, Up:rotate-yk-ptr body

剰余関数%

(% 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と表示される)。


Node:rotate-yk-ptr remainder, Next:, Previous:Remainder Function, Up:rotate-yk-ptr body

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要素に移動する。


Node:kill-rng-yk-ptr last elt, Previous:rotate-yk-ptr remainder, Up:rotate-yk-ptr body

最後の要素を指す

最後の疑問は、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はキルリングの要素を順番に指し、先頭に戻る。 リストには終わりがないかのように先頭に戻るので、 キルリングをリングとよぶのである (リングとは、終わりがないもの?)。


Node:yank, Next:, Previous:rotate-yank-pointer, Up:Kill Ring

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"を指定した対話的なものである。 これは、読み出し専用のバッファでは動作せず、 未処理の前置引数を関数に渡すことを意味する。


Node:rotate-yk-ptr arg, Next:, Previous:yank, Up:yank

引数の渡し方

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が指す先頭要素が挿入される。


Node:rotate-yk-ptr negative arg, Previous:rotate-yk-ptr arg, Up:yank

負の引数を渡す

最後に、剰余関数%や関数nthcdrに負の引数を与えると どうなるのであろうか?

試してみればわかる。 (% -1 5)を評価すると、負の数が返される。 負の数でnthcdrを呼び出すと、第1引数が0の場合と同じ値を返す。 つぎのコードを評価するとこのことがわかる。

=>のまえの式を評価すると=>のあとに 示した結果になる。 いつものように、コードの直後にカーソルを置いてC-x C-eeval-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よりも大きく戻る必要はないので、 マイナス記号に続く数の大きさを調べるように関数を書くよりもよほど簡単である。


Node:yank-pop, Previous:yank, Up:Kill Ring

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で挿入したテキストの先頭にポイントを置いてあった場合には、 ポイントとマークを入れ換えて、新たに挿入したテキストの先頭に 再度ポイントを置く。 以上である。


Node:Full Graph, Next:, Previous:Kill Ring, Up:Top

ラベル付きグラフ

座標軸が表示されていると、グラフの理解に役立つ。 これらは尺度を与えてくれる。 前節(see Readying a Graph)では、 グラフの本体を表示するコードを書いた。 ここでは、本体自身に加えて、ラベルを付けた垂直軸と水平軸も 表示するコードを書く。


Node:Labelled Example, Next:, Previous:Full Graph, Up:Full Graph

バッファへの挿入はポイントの右下へ向かって行われるので、 グラフを描く新しい関数では、まず、Y軸、つまり、垂直軸を描いてから、 グラフの本体を描き、最後に、X軸、つまり、水平軸を描く。 この描く順序から、関数の内容が決まる。

  1. コードの初期設定。
  2. Y軸を描く。
  3. グラフの本体を描く。
  4. 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の関数定義の各部分を順番に扱おう。


Node:print-graph Varlist, Next:, Previous:Labelled Example, Up:Full Graph

print-graphの変数リスト

関数print-graphを書く場合、最初の仕事は、 let式の変数リストを書くことである (関数を対話的にしたり、説明文字列の内容については、 しばらく手をつけないでおく)。

変数リストでは、数個の変数を設定する。 明らかに、垂直軸のラベルの先頭は少なくともグラフの高さである必要があるので、 この情報をここで得ておく必要がある。 関数print-graph-bodyもこの情報を必要とすることに注意してほしい。 異なる2つの場所でグラフの高さを計算する理由はないので、 ここでの計算を利用するようにprint-graph-bodyの以前の定義を変更する。

同様に、X軸を描く関数と関数print-graph-bodyの両者は、 各シンボルの幅を知る必要がある。 この計算もここで行い、print-graph-bodyの以前の定義を変更する。

水平軸のラベルの長さは少なくともグラフの幅である必要がある。 しかし、この情報は水平軸を描く関数でのみ使われるので、 ここで計算する必要はない。

以上の考察から、print-graphletの変数リストはつぎのようになる。

(let ((height (apply 'max numbers-list)) ; 第1版
      (symbol-width (length graph-blank)))

あとでわかるように、この式は実は正確ではない。


Node:print-Y-axis, Next:, Previous:print-graph Varlist, Up:Full Graph

関数print-Y-axis

関数print-Y-axisの仕事は、つぎのような垂直軸のラベルを描くことである。

    10 -




     5 -



     1 -

関数にはグラフの高さが渡され、適切な数字や目盛を作成して挿入する。

Y軸のラベルをどのようにすべきかは図からは簡単にわかるが、 言葉で書き表したり、そのようにする関数定義を書くことはまた別である。 5行ごとに数字や目盛が必要なのではない。 15のあいだには3行(2行目、3行目、4行目)あるが、 510のあいだには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の倍数である。


Node:Compute a Remainder, Next:, Previous:print-Y-axis, Up:print-Y-axis

余りの計算

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-graphlet式に埋め込む。

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


Node:Y Axis Element, Next:, Previous:Compute a Remainder, Up:print-Y-axis

Y軸の要素の作成

垂直軸を書くときには、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は、文字列を繋げる式で使われており、 数を文字列に変換する。 この文字列には、まえに置く空白文字や目盛が繋げられる。


Node:Y-axis-column, Next:, Previous:Y Axis Element, Up:print-Y-axis

Y軸のコラムの作成

これまでの関数は、 垂直軸のラベルとして挿入する数字や空白から成る文字列のリストを 生成する関数を作るために必要なすべての道具を提供する。

(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のあとに目盛を付ける。


Node:print-Y-axis Final, Previous:Y-axis-column, Up:print-Y-axis

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を試せる。

  1. インストールする。
    Y-axis-label-spacing
    Y-axis-tic
    Y-axis-element
    Y-axis-columnprint-Y-axis
    
  2. つぎの式をコピーする。
    (print-Y-axis 12 5)
    
  3. バッファ*scratch*に切り替え、 軸のラベルを書き始めたい場所にカーソルを移動する。
  4. M-<ESC>eval-expression)とタイプする。
  5. C-yyank)で、graph-body-print式を ミニバッファにヤンクする。
  6. <RET>を押して、式を評価する。

Emacsは、先頭が10 - であるようなラベルを垂直に表示する (関数print-graphheight-of-top-lineの値を渡し、 その値は15になる)。


Node:print-X-axis, Next:, Previous:print-Y-axis, Up:Full Graph

関数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段階の処理になる。

  1. X軸の目盛を描く関数print-X-axis-tic-lineを書く。
  2. X軸の数字を描く関数print-X-axis-numbered-lineを書く。
  3. print-X-axis-tic-lineprint-X-axis-numbered-lineを使って、 軸の2つの行を描く関数print-X-axisを書く。


Node:X Axis Tic Marks, Previous:print-X-axis, Up:print-X-axis

X軸の目盛

最初の関数は、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に値が設定されたかどうかを調べる。 設定されていない場合には、boundpnilを返す。 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-graphfull-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-lineprint-X-axis-numbered-lineを 使うprint-X-axisを書く必要がある。

関数では、print-X-axis-tic-lineprint-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を試してみよう。

  1. X-axis-tic-symbolX-axis-label-spacingprint-X-axis-tic-lineとともに X-axis-elementprint-X-axis-numbered-lineprint-X-axisをインストールする。
  2. つぎの式をコピーする。
    (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))))
    
  3. バッファ*scratch*に切り替え、 軸のラベルを描き始める場所にカーソルを置く。
  4. M-<ESC>eval-expression)とタイプする。
  5. C-yyank)でテスト用の式をミニバッファにヤンクする。
  6. <RET>を押して式を評価する。

Emacsはつぎのように水平軸を表示する。

     |   |    |    |    |
     1   5   10   15   20


Node:Print Whole Graph, Previous:print-X-axis, Up:Full Graph

グラフ全体の表示

グラフ全体を表示する準備ができた。

正しいラベルを付けたグラフを描く関数は、 まえに作った概略(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-graphlet式で 計算される。 そこで、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)))


Node:Test print-graph, Next:, Previous:Print Whole Graph, Up:Print Whole Graph

print-graphのテスト

関数print-graphを数の短いリストで試してみよう。

  1. Y-axis-columngraph-body-printprint-graph (および、残りのコード)をインストールする。
  2. つぎの式をコピーする。
    (print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1))
    
  3. バッファ*scratch*に切り替え、 軸のラベルを描き始めたい場所にカーソルを置く。
  4. M-<ESC>eval-expression)とタイプする。
  5. C-yyank)で、ミニバッファにテスト用の式をヤンクする。
  6. <RET>を押し、式を評価する。

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」)を表示するように ソースを直せばよい。)


Node:Graphing words in defuns, Next:, Previous:Test print-graph, Up:Print Whole Graph

単語やシンボルの個数のグラフ

グラフ作成に必要なコードはすべて書いた。 単語やシンボルの個数が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つの関数mapcarlambdaを使うと、 つぎの小さな関数でできる。

(defun one-fiftieth (full-range)
  "Return list, each number one-fiftieth of previous."
 (mapcar '(lambda (arg) (/ arg 50)) full-range))


Node:lambda, Next:, Previous:Graphing words in defuns, Up:Graphing words in defuns

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)から導かれたのである。


Node:mapcar, Next:, Previous:lambda, Up:Graphing words in defuns

関数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ではない)。


Node:Another Bug, Previous:mapcar, Up:Graphing words in defuns

別のバグ ... もっとも潜在的

「ほとんど整った」といったのは、関数print-graphにはバグがあるのである。 vertical-stepのオプションはあるが、 horizontal-stepのオプションはない。 top-of-rangeは、10間隔で10から300まである。 しかし、関数print-graphは、1ずつ描く。

これは、もっとも潜在的なバグの典型的なものであり、 無視したことによるバグである。 機能としては存在しないので、コードに書かれておらず、 コードを調べてもこの種のバグは発見できない。 最良の行動は、プログラムを初期の段階で何回もテストすることであり、 コードを可能な限り理解しやすく変更しやすくしておくことである。 たとえすぐにではなくても、最終的には、書いたコードは書き直すことになることを 理解しておいてほしい。 実行するのは難しい格言である。

関数print-X-axis-numbered-lineを直す必要がある。 そして、関数print-X-axisprint-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-axisprint-graphもあるので、これらを評価する。 印刷物で読んでいる場合には、変更した行をつぎに示してある (コードを全体は多すぎる)。


Node:Final Printed Graph, Previous:Graphing words in defuns, Up:Print Whole 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個の単語やシンボルを含むものである。


Node:Index, Next:, Previous:Full Graph, Up:Top

索引

Robert J. Chassellは、1985年以来GNU Emacsの仕事をしている。 執筆や編集、EmacsとEmacs Lispを教えており、 Free Software Foundation, Inc.の理事、幹事/会計でもある。 社会史や経済史に興味を持っており、自家用飛行機を操縦する。

 

Short Contents

Table of Contents


Footnotes

  1. ISBN4-88735-020-1、株式会社プレンティスホール出版

  2. 単語「argument」の異なる2つの意味、つまり、数学での意味と 日常語での意味がどのように成立したかを調べると興味深い。 Oxford English Dictionaryによれば、to make clear, prove (明らかにする、証明する)を意味するラテン語が語源である。 これから、「証明として与えられた証拠」という一方の意味が派生して、 「与えられた情報」を意味するようになり、Lispでの意味になるのである。 もう一方の意味としては、「ある仮定とは反対の仮定をする」を意味するようになり、 議論するという意味になったのである (英語では異なる2つの意味が同時に1つの単語に与えられている。 対照的に、Emacs Lispでは、異なる2つの関数定義を同時にシンボルに与えることは できない)。

  3. ドットペアーを作成するためにアトムと要素をconsすることも できる。 ここでは、ドットペアーについては 説明しないので、Dotted Pair Notationを参照。