Conao3 Notes

lispの同図像性とその周辺 ― leaf.el, seml-mode.elの内部構造に触れながら

はじめに

「EmacsのためにEmacsを使っている」事に最近気づいたconao3です。

とりあえず私の脳内スタックを紹介します。

  • 競プロしたい!
  • 当然Emacs使うでしょ
    • init.el編集して、 flycheck とか動かしたいな。。
    • 当然 use-package 使うでしょ
      • use-package がEmacs-22で動かない \arrow leaf.el 開発
        • Emacs-22で動くテストフレームワークが存在しない \arrow cort-test.el 開発
        • Emacs-22で動くパッケージマネージャが存在しない \arrow feather.el 開発
          • MELPAのレシピをあらかじめ変換しておきたい \arrow feather-recipes.el 開発
          • そもそもMELPAが提供しているjsonが壊れている \arrow feather-melpa 開発
          • .tar.gzをダウンロードさせるだけでいんじゃないか \arrow feather-server.el 開発
        • CUIベースの custom ではなく、Webブラウザベースで設定したい \arrow leaf-browser.el 開発
          • 当然HTMLを素で書く気にならないので、S式から変換させたい \arrow seml-mode.el 開発
    • org-publish のセクションが肥大化している \arrow orglyth.el 開発

つまりここに挙げたパッケージ群をちゃっちゃと開発し、どんどんスタックを pop していかないと、 「競プロしたい!」という当初の目的まで辿り着けないという状況になっています。辛い。

そもそもがinit.elの編集から木が深くなっているので、 leaf.el 開発当初はほぼ裸のEmacsで編集していました。 最近は leaf.el が結構便利になってきたので、快適に開発ができるようになってきています。

これだけのパッケージを開発していると、そこそこにアウトプットできるような知見が貯まってくるのですが、 今回、東京Emacs勉強会 端午の節句に15分の発表の機会を得ることが出来たので、 その時間を考慮して、そのなかでも「Lispの同図像性」について発表するものです。

Lispの評価器について

Lispの評価

Elispに限らず、lisp処理系は次のような仕組みを持っています。東京大学 - ソフトウェア第三 講義資料 - Lisp処理系, リスト処理, Paren-holic - 3つのLisp 3つの世界, STUART C. SHAPIRO - Common lisp - An Interacive approach, Harold Abelson 他 著 和田 栄一 訳 - 計算機プログラムの構造と解釈

「式」を次で定義します。

  • アトムを「数値」と「文字列」と「シンボル」とする。
  • コンスセルを「(car . cdr)」と記述し、carとcdrに「アトム」か「コンスセル」が代入されるとする。
  • リストを「(a b … x)」と記述し、「空リスト」か、「末尾のコンスセルのcdr部に nil が代入されたもの」とする。
  • 式を「アトム」か「コンスセル」か「リスト」さらに「式のリスト」とする。

なお、コンスセルのドットは字面上の意味がありますが、リストの3点リーダはシンボル群の略記です。 さらに「 nil 」は「アトム」であり、「空リスト」であるとします。

そして与えられた式を次のように評価 (eval) します。Richard Stallman 他 著 Ayatakesi 訳 - GNU Emacs Lisp Reference Manual

  1. 式が、アトムの場合、以下のように処理する。
    1. シンボルならば、そのシンボルの値を返却する。
    2. それ以外(数値、文字列など)ならば、それ自身を返却する。
  2. 式が、リストフォームならば、リストフォームの先頭データの種類によって、以下のように処理する。
    1. スペシャルフォームならば、第2引数以降のデータを評価せずに渡し、それぞれ固有の処理を行い、結果を返却する。
    2. マクロならば、第2引数以降のデータを評価せずに渡し、評価後のS式を改めて評価 (eval) し、結果を返却する。
    3. 関数ならば、第2引数以降のデータを順に評価 (eval) した上で、関数に渡 (apply) し、結果を返却する。

スペシャルフォームとは処理系内部にLispとは異なる言語で定義されたものです。 Elispの場合、C言語で実装されている関数はすべてスペシャルフォームです。

スペシャルフォームの最たる例は条件分岐 (if COND THEN-PART ELSE-PART...) で、 COND が真の場合、 ELSE-PART は評価されません。 この性質は関数の場合と決定的に異なっており、例えば条件によって0除算エラーが起こってしまうなどのクリティカルな状況を回避することが出来ます。

条件分岐のような制御構造は通常の言語であれば、拡張不可(処理系のアップデートが必要)ですが、Lispであればマクロを使用することで自分で定義することが出来ます。

3種類のマクロ

  1. リーダマクロ
  2. マクロ
  3. コンパイラマクロ

leaf.elの概略

leaf.elはjwiegley氏のuse-packageと同様に、Emacsのinie.elで頻出のイディオムを隠蔽し、編集しやすく、そして管理しやすくするものです。 use-package はとても良く出来ていて、愛用していたのですが、問題もありました。(省略するので、別途Qiita記事を参照)

  • :if :disabled で無効化されているパッケージがなぜか読み込まれている。
  • 新しいキーワードを追加するのが困難。
  • あるパッケージだけ違うディストリビューションからダウンロードできない。
  • (use-packageに依存したinit.elをEmacs-22で読み込むと骨抜きになる。)

主に最初の3つが大きな不満点でした。 最後の点は許容できたのですが、作り直すなら縛りプレイをすることにしました。

さて、説明のために次の簡単なマクロを定義した上で、

(defmacro p (form)
  `(progn
     (pp (macroexpand-1 ',form))
     nil))

(defmacro po (form)
  `(progn
     (pp ,form)
     nil))

leaf.el は次のような変換をするマクロを提供するものです。

(p
 (leaf yasnippet
   :ensure t
   :custom ((yas-indent-line . 'fixed))
   :bind (:map yas-minor-mode-map
               ("C-c y i" . yas-insert-snippet)
               ("C-c y n" . yas-new-snippet)
               ("C-c y v" . yas-visit-snippet-file)
               ("C-c y l" . yas-describe-tables)
               ("C-c y g" . yas-reload-all))
   :config
   (leaf yasnippet-snippets :ensure t)
   (leaf yatemplate :ensure t)
   (yas-global-mode 1)))
;; => (progn
;;      (leaf-meta-backend/:ensure 'yasnippet '(t))
;;      (leaf-meta-backend/:bind 'yasnippet
;;                               '((:map yas-minor-mode-map
;;                                       ("C-c y i" . yas-insert-snippet)
;;                                       ("C-c y n" . yas-new-snippet)
;;                                       ("C-c y v" . yas-visit-snippet-file)
;;                                       ("C-c y l" . yas-describe-tables)
;;                                       ("C-c y g" . yas-reload-all))))
;;      (custom-set-variables '(yas-indent-line 'fixed))
;;      (leaf yasnippet-snippets :ensure t)
;;      (leaf yatemplate :ensure t)
;;      (yas-global-mode 1))
(p
 (leaf yasnippet
   :disabled t
   :ensure t
   :custom ((yas-indent-line . 'fixed))))
;; => nil

leaf はマクロなのでElisp処理系は、実際の評価の前にマクロが展開し、その後、評価器が実際の評価を行います。

Footnotes

1 東京大学 - ソフトウェア第三 講義資料 - Lisp処理系, リスト処理

2 Paren-holic - 3つのLisp 3つの世界

3 STUART C. SHAPIRO - Common lisp - An Interacive approach

4 Harold Abelson 他 著 和田 栄一 訳 - 計算機プログラムの構造と解釈

5 Richard Stallman 他 著 Ayatakesi 訳 - GNU Emacs Lisp Reference Manual