gofujita notes

on outline processing, writing, and human activities for nature


Org との日々 #10

6. 自己記述しながら機能拡張するエディタ

EmacsWiki のページには、こんなことがかかれています。

Emacs is an extensible self-documenting editor (Emacs は、自己記述しながら機能拡張するエディタである).

これを鵜呑みにすると、Emacs は Emacs でかかれ、Emacs でつくりつづけられているエディタ、ということになるでしょうか。

Emacs はテキストエディタでありながら Emacs 自体を開発する総合開発環境 IDE でもあるのです。

Emacs は、1976年に誕生してから一貫して、ユーザーが使いながら Emacs そのものも開発できる環境づくりを目指しつづけてきたテキストエディタなのだと、ぼくは理解しています。

 

6-a. Emacs の95%は Emacs Lisp でできている

ぼくは Smalltalk ファンです。

それは、1970年初頭にゼロックスのパロアルト研究所で生まれた最初のオブジェクト指向プログラミング言語のひとつ。1978年にパロアルト研究所を訪れた Steve Jobs が Smalltalk-76 に出会い、それが GUI をもつ Apple Lisa (1983年) や Macintosh (1984年) 誕生のきっかけになったとされる、あの Smalltalk です。

パロアルト研究所で、Smalltalk の生みの親とされる Alan Kay たちが、「あらゆる世代の子どもたち」をターゲットにしたプログラミング環境づくりを目指していた、という文章 (Kay 1972) をよんだときのドキドキは、今もよく覚えています。

この Smalltalk にも、プログラミング言語に加え、コンパイラやデバッガ、そして自前の GUI を備えた総合開発環境が備わっています。

その後、世界に大きな影響を与えることになる Smalltalk の GUI も、みんながつかえるパーソナルコンピュータを目指した開発の途中に誕生した機能だと、理解しています (Kay 1993, Ingalls 2020)。

Emacs と Smalltalk は、過去をみても長く別の道を歩んできたようにみえますし、外見や中身、実装もちがうどころか真逆のアプローチをとっているようにみえる部分もあります。

しかし、つかいながらそのシステムを改良しつづけられるという点で、Emacs と Smalltalk は、たがいに似た存在であると、ぼくは予想しています。

たとえば、両者の共通点として OS やアプリケーションというしばりがない、という特徴があります。どちらも総合開発環境であり、OS であり、アプリでもある。あるいは、OS とアプリという概念が希薄。これも、つかいながら変化させられるシステムを目指しているからこその特徴と思っています。

Emacs の話しに戻りましょう。

Emacs は、Emacs Lisp (elisp) という Lisp 系言語をつかってその環境をつくることで、この「自己記述しながら機能拡張するエディタ」を実現しています。

核になる機能は C 言語でかかれているそうですが、その割合は全体の5%。残りの 95% は elisp でできているそうです。Emacs のメジャーモードのひとつである Org は、すべて elisp でできています。

elisp の直接の祖先にあたる Lisp は、1958年生まれの古い言語で、John McCarthy という AI の研究者がデザインしました (McCarthy 1979)。Lisp は FORTRAN についで2番目に古い、そして COBOL とほぼ同時期に誕生した高級プログラミング言語 (自然言語に近い、人が理解しやすいカタチをした言語) とされています。

そのコードは、括弧でくくられた変わった見かけをしていますが、Lisp の大切な特徴として、その場で小さく実行し、結果を確かめられる点があります。

もしあなたが Emacs をつかっているなら、たとえば、以下のようなコードをかいてみてください。

(* 7 (+ 5 (- 3 1)))

これを、ぼくたちが見慣れている算数の式にかき直すと、こうなります。

7 × (5 + (3 − 1))

Lisp では、かけ算や足し算、引き算などの記号を数字の前に置き、まず最初にどんな仕事をするのか (かけるのか,足すのか、引くのか) をインタープリタに伝え、そのあとに処理したい数字がスペースで区切られて、置かれています。

Emacs では、上のコードのいちばん最後の括弧のすぐうしろにカーソルを置いてC-x C-e とタイプすることで、その計算を実行できます。

結果は、いちばん下の小さな窓、エコーエリア echo area に出ます。49ですね。

次に、ひとつ内側の括弧(+ 5 (- 3 1))の最後の括弧のうしろにカーソルを置いて同じようにC-x C-eしましょう。この場合、カーソル直前の括弧内の式が計算されます。結果は 7。

同じようにさらに内側にある括弧、(- 3 1)のうしろにカーソルを置いて実行すると 2 という結果が得られます。

Lisp では、複雑な構造のコードでも、括弧を単位にその階層構造をつかって処理する仕組みがあり、内側 (下位階層にある) それぞれの括弧の中身を部分的に処理してその結果をひとつずつチェックできるワケです。

この例ではかけ算や足し算の関数でしたが、他の機能をもつ関数でも同じことができます。部分ごとに処理をした結果を確かめながら、コード全体を組み上げられるのです。

 

6-b. 関数をつくるdefun

では、実際にコードや文章の編集でつかわれている elisp のコードをのぞいてみましょう。

たとえばぼくが、指定範囲内の文章の改行マークを一括削除する関数をつくりたいとします。

まずはC-k fでざっと既存の関数リストを探したけど、そんな機能をもった関数が見つからない。で、つぎはオンライン検索。「emacs function remove line break」という5つのキーワードで Google すると、運よくすぐに以下のページがみつかりました (しめしめ)。

How to remove all newlines from selected region in Emacs?

このページトップにある質問への3つ目の答えが、ぼくの希望に近い情報です。Mark Longair さんがかいたコードです (ありがとう)。

  
  (defun remove-newlines-in-region ()
     "Removes all newlines in the region."
     (interactive)
     (save-restriction
        (narrow-to-region (point) (mark))
        (goto-char (point-min))
        (while (search-forward "\n" nil t) (replace-match "" nil t))))
   

この Mark さん手づくりの関数を題材に、elisp をつかった新しい関数を定義する手順と、指定範囲内の改行マーク削除という仕事を elisp で実装するカタチを具体的に紹介します。

最初に、この関数の仕事っぷりを試します。

いちばん最後の括弧の直後にカーソルを置いて、いつものようにC-x C-eで実行します。

コード全体が瞬間グリーンにハイライトされ、画面いちばん下の echo area にこの関数名remove-newlines-in-regionが表示されれば、この関数のインストール成功。 Emacs ではこの状態を、関数をインストールした、と呼びます。

つぎに、以下のようなテスト用データをかきます。

そして、このデータすべてを選択。

つぎにM-xとタイプし、インストールされている関数リストの窓が画面下に開いたら、その窓のいちばん上にある minibuffer に表示された「M-x」の右に関数名remove-newlines-in-regionを入力してreturnで実行。

データの形が変化します。

まずは、期待どおりの仕事っぷり。指定範囲内のすべての改行がなくなっています (やったぜ)。

さぁ、いよいよ定義するコードをみて行きます。elisp では、関数を定義する機能としてdefunが使われます。defunは、大きく5つの部品でできています。

  • 1: defun
  • 2: 定義する関数の名前とその引数の名前。引数名は( )の中にかく
  • 3: 定義する関数の説明。" "の中にかく。C-h fでよびだす関数の説明に表示される
  • 4: 定義する関数が対話型かどうか
  • 5: 定義する関数のふるまいをかいたコード本体

2 の「引数」は「ひきすう」とよみます。関数が必要とする変数 (数や文字などのデータ) があれば、ここにその名前を入れます。ない場合は空白。

remove-newlines-in-regionは変数なしで仕事するので、括弧の中を空白にしています。ちなみに、Lisp ではこの空白をnilと呼びます。そして、ちょっとややこしいのですが、nilが真偽値の「偽」の役割を担っています。

つぎの 4 に(interactive)とかけば、この関数が対話型になります。対話型関数にすることで、たとえばM-xとタイプして現われる関数リストにも入り、キーバインディングもできるようになります。

5 のコード本体は、少し長くなるので節を改めて説明します。

 

6-c. 階層構造としてコードを理解する

コード本体を、もういちど抜き出してみてみましょう。


    (save-restriction
       (narrow-to-region (point) (mark))
       (goto-char (point-min))
       (while (search-forward "\n" nil t) (replace-match "" nil t))

このコードが、つぎのような構造をもっていることが分かるでしょうか。

  • 関数A save-restriction
    • 関数B narrow-to-region
    • 関数C goto-char
    • 関数D while
      • 関数E search-forward
      • 関数F replace-match

で、この階層に沿って、この本体をみて行きます。そして、まずはいちばん下位にある関数から、ボトムアップなスタイルでよみすすめましょう。関数EとFです。

 

関数E(search-forward 引数1 引数2 引数3)

括弧の中の先頭にあるsearch-forwardは関数の名前を示すシンボル。Emacs のインタープリタは先頭にあるこのシンボルをよんで、自分の仕事を理解します。

Emacs では、インストールされた関数がどんな仕事をするのか、C-h fでしらべることができます。Spacemacs では、C-h fとタイプすると「Describe function (関数の説明)」という機能の画面が現われ、カーソルが画面上端の minibuffer にある「Describe function: 」という文字の先 (右側) へ移動します。

そこにsearch-fowardと入力しリターンキーを押すと、その説明が表示されます。

その5行目と6行目につぎのような説明があります (図の赤枠)。

  • point から前方へ向かって STRING を探す
  • point を見つけた文字列の最後に置いて、point を返す

これを、ぼくが理解した範囲でかき換えると..。

  • point (カーソル) の位置からから前方へ移動しながら STRING を探す
  • STRING はこの関数がつかう1番目の引数で" "でくくった文字列。今回は\n、改行マークです (Meiryo などのフォントではバックスラッシュ\が円マークで表示されます)
  • \nを見つけたら、カーソルをそこへ移動し、その位置の値をインタープリタに伝える

2つ目の引数として、ここではnilがかかれています。この引数は検索範囲を制限するもので、オプション。つまり、この引数をつけない場合もある。今回はnilなので制限なし、つまり選択範囲の最後まで調べてください、という意味。

3つめの引数もオプションで、何も見つけられなかった場合のふるまいを決めています。tの場合は、エラーではなく、nilを返すという意味。

まとめると、この関数Eは次のような Emacs インタープリタへのメッセージだと理解できます。

  • 最初にカーソルのある位置から前方へ移動しながら、\nの文字を探し、みつけたらその位置でとりあえず止まる
  • ただし、そこで探索を終えずに、次の出番がきたらまた探索してそこで止まる、という作業を選択範囲の終わりまでつづける
  • 選択範囲内に\nがひとつもみつからなくてもエラーにせず仕事を終了させる
  • 終了時にnilをインタープリタに送る

 

関数F(replace-match 引数1 引数2 引数3)

関数F(replace-match 引数1 引数2 引数3)は、search-fowardがみつけた文字列を別の文字列に置換します。引数3つが気になりますね。C-h fでしらべると説明があります。いろいろ機能があるのですが、今回使われているものにしぼると以下が分かりました。

  • 直前の検索で見つけた文字列 (この場合は、すぐ左にあるsearch-forwardで見つけた文字列) を、最初の引数 (""で囲んだ文字列。この場合は何もないので消去することになる) で置換する
  • 2番目の引数はオプション。nilでない場合は大文字と小文字を区別しない
  • 3番目の引数もオプションで、nilでない場合は、文字どおりに挿入する

 

関数D(while (引数1) (引数2))

ひとつ上の階層になる関数Dの(while (引数1) (引数2))のメッセージは、何となく分かりますね。

C-h fでしらべると、予想どおりでした。引数1が真のあいだ、引数2の作業をつづける。つまり、(search-forward ..)が指定範囲内にある\n を検索し終わるまで、(replace-match ..)の置換をつづける、というのが関数Dの仕事です。今回つくっている関数のいちばん中心になる部品ですね。

 

関数B(narrow-to-region (引数1) (引数2))と関数C(goto-char (引数1))

同じようにC-h fをつかって、関数Dと同じ階層にある関数BとCの機能をしらべ、大体こんなことが分かりました。

  • 関数B(narrow-to-region (引数1) (引数2)): 編集範囲を引数1から引数2の範囲に指定する
    • 引数1(point): カーソルの位置
    • 引数2 (mark): 選択範囲のマークした位置。この場合は選択範囲の最後の位置
  • 関数C(goto-char (引数1)): point つまりカーソルの位置を引数1の位置へ移動する
    • 引数1 (point-min): この場合、選択範囲内のいちばん最初の位置

関数BからDまでの仕事をまとめると、こうなります。

  • 関数Bで作業する範囲を選択範囲に指定し
  • 関数Cでカーソルを選択範囲の先頭へ移動
  • 関数Dで選択範囲の改行マークをひとつずつ検索しながら削除

 

関数A(save-restriction ..)

さて最後に関数A(save-restriction ..)。その説明にかいてあることを、ぼくなりにえいやとまとめてみます。

  • 現在 (この処理をはじめるとき) の buffer の範囲を保存し、本体の処理 (この場合、関数BからDの処理) によって変化した buffer 範囲を、処理が終わった段階で元に戻す

問題はこの buffer が何か。たぶん、この処理を始める段階でフォーカス narrowing している範囲を指していると予想してます。

つまり、関数Bで変更した narrowing の範囲を、関数CとDの処理が終わったあと、最初の範囲にもどす役割を、この関数Asave-restrictionが担っていると、ぼくは理解しました。

このsave-restrictionのおかげで、narrowing の範囲を、この場合だと関数B–Dの操作内容に関係なく、同じカタチに維持できます。

ちなみに、このremove-newlines-in-regionの場合、このsave-restrictionを削除すると、narrowing の範囲が選択範囲だけに狭まってしまいます。関数Bnarrow-to-regionが指定した範囲です。

余談になりますが、アウトライナーでフォーカス focus あるいはズーム zoom とよばれる動作は、ユーザーが画面上にみているデータの範囲を限定する操作です。でも実は、それが表示データを処理するアプリにとっても、処理範囲を限定するという大切な役割を担っていることを、このsave-restrictionという関数をしらべながら、気づくことができました。

アウトライナーの強みは、データがアウトラインという構造をもち、それを維持できる点だと思います。アウトライン操作によって、アプリに処理してもらう範囲も自由に操作できる可能性をもっていると、ぼくは考えています。

ただ、その特徴をいかしたアプリや機能の開発は、あまり進んでいないように思います。

Mark Longair さんのつくった関数remove-newlines-in-regionをみながら、関数を定義する関数defunや、その部品になっているいくつかの関数の機能をしらべてみました。

Emacs では、C-h fをつかって関数の機能を Emacs の中でしらべることできること、それをよみながら、先人たち、そして仲間たちがつくった関数を学び、自分の関数づくりにも役立てられるワケです。

 

長くなってしまったので、今回はここまでにしましょうか。

次回では、このremove-newlines-in-region関数を、自分の望むカタチにかき換えたり、手づくり関数を継続的に使えるよう設定ファイルにかきこんだりする手順を紹介します。