Gnu Emacs 拡張ガイド: Emacs Lispプログラミング まとめ


GNU Emacs拡張ガイドは既に絶版した本らしいですが、一年くらいAmazonのマーケットプライスで買って,本の内容を色々と設定した後しばらくの間放置していたのですが久々に見つけたのでなんとなくまとめてみます。内容はEmacs Lispの実践的なtips集とでもいうべきか. これを一通り読めばEmacs Lispのソースが読める + マイナーモード or メジャーモードが作れるくらいにはなるかと思います。古い本ですがまあなかなか内容はいいんじゃないかと。

とりあえず始めに目次と概要について触れときます

  • 1章 Emacsをカスタマイズする
    • 簡単なカスタマイズ + aproposコマンド重要という内容
  • 2章 簡単なコマンドを作る
    • 簡単だけれども非常に役に立つコマンドの作成. Lispの解説をしつつ実装. 実際この手のコマンドはあまり見かけないが結構便利.
  • 3章 コマンド間の強調
    • 要するにundoっぽい事をやりたいという感じ。unscroll
  • 4章 バッファのなかを検索し変更する
    • 4章からそこそこ本格的内容になってくる。bufferの中を検索しある条件を満たした箇所におけるタイムスタンプの更新を行うという感じの内容。 save-excursion, save-restriction, save-match-data, search-forward, Lispにおける正規表現の使い方に関する説明など。
  • 5章 Lispファイル
    • 4章の内容を踏まえた内容. 4賞で作った内容をtimestam.elというファイルにまとめる。それを踏まえて、Lispファイルの作り方 + load, require, autoload, eval-after-loadの違いに関する説明 + バイトコンパイルについての説明がなされている。
  • 6章 リスト
    • リストの説明。 Lispをそこそこ知っている場合スルー可
  • 7章 マイナーモード
    • 7章以降は何らかのモードを実装するという内容になっています。 7章ではモードに関する説明 + refillモードという段落の行詰めをするマイナーモードを実際に実装していたりします。
  • 8章 評価とエラー処理
    • 7章の内容を踏まえた内容save-excursionは結構重い処理をするのでlimited-excursionというマクロを実装する。マクロの説明が多い。
  • 9章 メジャーモード
    • メジャーモードの説明とquip-mode(名言編集モード)の実装.ナローイングと派生モードの説明など
  • 10章 総合的な例
    • これまでの応用というわけでクロスワードパズルっぽいメジャーモードを作りまする


以降は具体的な内容になります

1章 Emacsをカスタマイズする

特になし, apropos重要



2章 便利なコマンドを作成する

タイトルの通り便利なコマンドを作っていく

  • その1 other-window-backward
    • other-windowというコマンドは標準で搭載されているがother-window-backwardはないのでそれの実装を行う

具体的には以下のような実装をする

(defun other-window-backward (&optional n)
  "Select nth previous window."
  (interactive "P")
  (other-window (- (prefix-numeric-value n))))

;; key settings
(global-set-key "\C-x\C-p" 'other-window-backward)
(global-set-key "\C-xp" 'other-window-backward)

other-window-backwardは以下のようにもかけるがEmacs Lisp的には上記のコードの方が良いとの事。実際Emacs Lispのソースは(interactive "p")ではなく(interactive "P")を使っている傾向がある気がする

(defun other-window-backward (&optional n)
  "Select nth previous window."
  (interactive "p")
  (other-window (- (or n 1))))
  • その2 一回に一行のスクロール

内容的にはvim, っぽい機能を実装するという感じです。実装内容は以下の通り. global-set-keyのC-zはなにかしらかぶっている可能性が高いので別のキーに設定しておいた方が良いかも

(defalias 'scroll-ahead 'scroll-up)
(defalias 'scroll-behind 'scroll-down)
 
(defun scroll-n-lines-ahead (&optional n)
  "Scroll ahead N lines."
  (interactive "P")
  (scroll-ahead (prefix-numeric-value n)))

(defun scroll-n-lines-behind (&optional n)
  "Scroll behind N lines."
  (interactive "P")
  (scroll-behind (prefix-numeric-value n)))

(global-set-key "\C-q" 'scroll-n-lines-ahead)
(global-set-key "\C-z" 'scroll-n-lines-behind)
  • その3 その他のカーソル移動コマンド

その2以外のカーソル移動コマンド。以下の設定はいずれも結構便利です。
とりあえず実行してみるといいんじゃないですかね

(defun point-to-top()
  "put point on top line of window"
  (interactive)
  (move-to-window-line 0))

(global-set-key "\M-," 'point-to-top)

(defun point-to-bottom()
  "put point at beginning of last visible line."
  (interactive)
  (move-to-window-line -1))

(global-set-key "\M-." 'point-to-bottom)


(defun line-to-top()
  "Move current line to top of window"
  (interactive)
  (recenter 0))

(global-set-key "\M-!" 'line-to-top)
(global-set-key "\C-ct" 'line-to-top)
  • その4 シンボリックを上書き
    • ファイルを読み込んだ時にそれがシンボリックリンクならバッファを読み込み専用にして"File is a symlink"というmessageを送るという趣旨
(defun read-only-if-symlink ()
  (if (file-symlink-p buffer-file-name)
      (progn
	(setq buffer-read-only t)
	(message "File is a symlink"))))

(add-hook 'find-file-hooks 'read-only-if-symlink)

以下はシンボリックリンクを扱うための二種類の関数。

(defun visit-target-instead ()
  "Replace this buffer with a buffer visiting the link target."
  (interactive)
  (if buffer-file-name
      (let ((target (file-symlink-p buffer-file-name)))
	(if target
	    (find-alternate-file target)
	  (error "Not visiting a symlink")))
    (error "Not visiting a file")))

(defun clobber-symlink()
  "Replace symlink with a copy of the file."
  (interactive)
  (if buffer-file-name
      (let ((target (file-symlink-p buffer-file-name)))
	(if target
	    (if (yes-or-no-p (format "Replace %s with %s? "
				      buffer-file-name
				      target))
	  (progn
	    (delete-file buffer-file-name)
	    (write-file buffer-file-name)))
	(error "Not visiting a symlink")))
  (error "Not visiting a file")))

(global-set-key "\C-xt" 'visit-target-instead)
(global-set-key "\C-x1" 'clobber-symlink)
	    
  • その5 その次はバッファの切替時にアドバイスを与えるという内容,
  • defadviceを使用する。defadviceはある関数が実行される前か後に実行するプログラムを指定出来るという機能。まあトリガーですね。名称謎
  • やりたい事はbuffer切替時に現状存在しないbufferを読み込まないようにするというもの。 ソースは以下の通り。
(defadvice switch-to-buffer (before existing-buffer
				    activate compile)
  "when interactive, switch to existing buffers only, unless given a prefix argument."
  (interactive
   (list (read-buffer "Switch to buffer:"
		      (other-buffer)
		      (null current-prefix-arg)))))

まあそこそこ便利。


3章 コマンド間の協調

undoっぽい事を実装するんですが,あまり必要ないかも。まああったらあったで便利といえば便利

間違えてscroll-upなどを実行したときに元の場所に戻すコマンドが欲しいという感じの趣旨。

まあ取り敢えずざっとでてくるElispを載せておきます。最終型だけなのでかなり略しとります

(defvar unscroll-point (make-marker)
  "Cursor position for next call to 'unscroll'.")

(defvar unscroll-window-start (make-marker)
  "Window start for next call to 'unscroll'.")

(defvar unscroll-hscroll nil
  "Hscroll for next call to 'unscroll'.")

(defadvice scroll-up (before remember-for-unscroll
			     activate compile)
  "Remember where we started from, for 'unscroll'."
  (unscroll-maybe-remember))

(defadvice scroll-down (before remember-for-unscroll
			     activate compile)
  "Remember where we started from, for 'unscroll'."
  (unscroll-maybe-remember))

(defadvice scroll-left (before remember-for-unscroll
			     activate compile)
  "Remember where we started from, for 'unscroll'."
  (unscroll-maybe-remember))

(defadvice scroll-right (before remember-for-unscroll
			     activate compile)
  "Remember where we started from, for 'unscroll'."
  (unscroll-maybe-remember))

(put 'scroll-up 'unscrollable t)
(put 'scroll-down 'unscrollable t)
(put 'scroll-left 'unscrollable t)
(put 'scroll-right 'unscrollable t)


(defun unscroll-maybe-remember ()
  (if (not (get last-command 'unscrollable))
      (progn
	(set-marker unscroll-point (point))
	(set-marker unscroll-window-start (window-start))
	(setq unscroll-hscroll (window-hscroll)))))

(defun unscroll ()
  "Revert to 'unscroll-point', 'unscroll-window-start' and 'unscroll-hscroll'."
  (interactive)
  (goto-char unscroll-point)
  (set-window-start nil unscroll-window-start)
  (set-window-hscroll nil unscroll-hscroll))

3章のtips的な内容は以下の通り

  • defvarについてあれこれ
    • defvarではdefunのように変数に説明をつけることが出来る。
    • 変数にデフォルト値を与える事が出来る
    • タグ関係のコマンドで変数宣言を見つけることが出来る
    • プログラムをバイトコンパイルした時defvarで宣言していない変数が見つかると警告を発する。
  • goto-char, set-window-start, set-window-hscrollはそれぞれカーソルの位置の設定、ウインドウの開始位置の設定、水平方向のスクロール状態を設定出来る。
  • point, window-start, window-hscrollはそれぞれ現在の位置、ウインドウの位置、水平方向の位置を保持しているグローバル変数
  • 変数や関数にシンボル属性をつけておくと便利な事がある。設定はput, 取得はget
  • last-commandは前回実行したコマンドを記憶している変数, this-commandは現在実行しているコマンドの内容を保持している変数である。
  • ポジション関連の操作をする際にはマーカーは便利。なぜならバッファの編集も考慮してくれるから。

まあこんなところ。

4章 バッファのなかを検索し変更する。

  • 時刻に関わりのある関数の説明 + バッファを検索して正規表現を利用してある条件を満たした箇所に現在時刻を加えるという内容。いろいろなテクニックの解説、特に正規表現
    • ファイルが新たに更新する度にライトスタンプが更新される。

とりあえず順番にみていきます。

以下は単に現在の時刻を書き加えるコマンド。関数に出てくる(interactive "*")の意味は、もしカレントバッファが読み込み専用なら関数の実行を中止するという事。

(defun insert-current-time()
  "Insert the current time"
  (interactive "*")
  (insert (current-time-string)))


次に変数の定義。注目すべき事としては、defvarの定義において*が始めにきた場合はその変数が対話的に変更出来るという点。\\[insert-time]はそのまま文字列として現れることはなくinsert-timeのキーマップに置き換えられるという点があげられます。キーが割り当てられてない場合にはM-x insert-timeが使われます。

(defvar insert-time-format "%H:%M"
  "*Format for \\[insert-time] (c.f. 'format-time-string').")
(defvar insert-date-format "%m-%d"
  "*Format for \\[insert-date] (c.f. 'format-time-string').")
  

次にinsert-time, insert-dateを作成する

(defun insert-time ()
  "Insert the current time according to insert-time-format."
  (interactive "*")
  (insert (format-time-string insert-time-format
			      (current-time))))

(defun insert-date ()
  "Insert the current date according to insert-time-format."
  (interactive "*")
  (insert (format-time-string insert-date-format
			      (current-time))))
  • 次にライトスタンプという関数を実装します。。ソースは以下の通り。
    • (add-hook 'local-write-file-hooks 'update-writestamps)により、ファイルをセーブする度に'update-writestampsが呼ばれたりします。
(defvar writestamp-format "%Y-%m-%d %H:%M"
  "*Format for writestamps (c.f. 'format-time-string').")
(defvar writestamp-prefix "WRITESTAMP(("
  "*Unique string identifying start of writestamp.")
(defvar writestamp-suffix "))"
  "*String that terminates a writestamp.")

(add-hook 'local-write-file-hooks 'update-writestamps)
(defun update-writestamps ()
  "Find writestamps and replace them with the current time."
  (save-excursion
    (save-restriction
      (save-match-data
	(widen)
	(goto-char (point-min))
	(let ((regexp (concat "^"
			      (regexp-quote writestamp-prefix)
			      "\\(.*\\)"
			      (regexp-quote writestamp-suffix)
			      "$")))
	  (while (re-search-forward regexp nil t)
	    (replace-match (format-time-string writestamp-format
					       (current-time))
			   t t nil 1))))))
  nil)


次にモディファイスタンプという物を作ります。これは、ファイルが最後に変更された時刻を記憶するライトスタンプであります。

作業の流れは以下の通りです。

  • 手法その1 first-change-hookの利用
    • バッファが再後に保存されて以来始めてバッファが変更される場合はいつもfirst-change-hookのなかの関数が実行されるのでこれを利用する
    • 問題点はファイルを保存する前にいくらか変更を加えた場合、最初の変更時間が適用されるという点にあります。
(make-local-hook 'first-change-hook)

(defvar modifystamp-format "%Y-%m-%d %H:%M"
  "*Format for writestamps (c.f. 'format-time-string').")
(defvar modifystamp-prefix "WRITESTAMP(("
  "*Unique string identifying start of writestamp.")
(defvar modifystamp-suffix "))"
  "*String that terminates a writestamp.")

(defun update-modifystamps ()
  "Find writestamps and replace them with the current time."
  (interactive)
  (save-excursion
    (save-restriction
      (save-match-data
	(widen)
	(goto-char (point-min))
	(let ((regexp (concat "^"
			      (regexp-quote writestamp-prefix)
			      "\\(.*\\)"
			      (regexp-quote writestamp-suffix)
			      "$")))
	  (while (re-search-forward regexp nil t)
	    (replace-match (format-time-string writestamp-format
					       (current-time))
			   t t nil 1))))))
  nil)

(add-hook 'first-change-hook 'update-modifystamps)

  • 手法その2 local-write-file-hooksの利用
    • バッファが保存される度に使われるlocal-write-file-hooksを利用する。ライトスタンプで実行済み
    • この手法の問題点はファイルが保存されるまで最終時刻が変更されない点にあります。
(defun maybe-update-modifystamps ()
  "Call 'update-modifystamps' if the buffer has been modified."
  (if (buffer-modified-p)
      (update-modifystamps)))

(add-hook 'local-write-file-hooks 'maybe-update-modifystamps)

  • 手法その3 賢い方法
    • 賢い。 ここまで到達するには色々と考える必要がある。。
    • 色んなエッセンスが凝縮されてる感じ。
(make-local-hook 'after-change-functions)

(defvar last-change-time nil
  "Time of last buffer modification.")

(make-variable-buffer-local 'last-change-time)

(defun remember-change-time (&rest unused)
  "Store the current time in 'last-change-time'."
  (setq last-change-time (current-time)))
  
(add-hook 'after-change-functions 'remember-change-time)


(defun maybe-update-modifystamps ()
  "Call 'update-modifystamps' if the buffer has been modified."
  (if last-change-time
      (update-modifystamps)))


(add-hook 'local-write-file-hooks
	  '(lambda ()
	     (if last-change-time
		 (update-modifystamps last-change-time))))

(defun update-modifystamps (time)
  "Find writestamps and replace them with the current time."
  (interactive)
  (save-excursion
    (save-restriction
      (save-match-data
	(widen)
	(goto-char (point-min))
	(let ((regexp (concat "^"
			      (regexp-quote writestamp-prefix)
			      "\\(.*\\)"
			      (regexp-quote writestamp-suffix)
			      "$")))
	  (while (re-search-forward regexp nil t)
	    (replace-match (format-time-string writestamp-format
					       time)
			   t t nil 1))))))
  (setq last-change-time nil)
  nil)

この章におけるtipsについては以下にまとめておきます。

  • バッファ保存に関わるhookとしてはwrite-file-hooks, local-write-file-hooks, write-contents-hooksなどがある
  • 文字列内で,\\[insert-date]というのはinsert-dateに割り振られているコマンドを表示したい場合に使う
  • (interactive '*')はカレントバッファが読み込み専用なら関数の実行を中止する
  • save-excursionはカーソルの位置を覚え,引数としての処理を評価した後,元の位置に戻す
  • save-restrictionはナローイング(あるbufferにおいて一部のbufferのみを処理する場合に使う) 状態を覚えておいて,最終的に元のナローイングに戻す。(widen)とかと併用される事が多い。
  • save-matchは検索結果を保持する
  • save-excursion, save-restriction, save-matchは結局の所,処理内容は非常に良く似ていたりする。
  • Lisp正規表現は他の言語とは少し違う. regexp-quoteを使うと便利

5章 Lispファイル

load, require, auto-load, eval-after-load,バイトコンパイルなどの説明

以下に内容をまとめておきます。

  • load
    • 明示的なload. どうしてもロードが必要な時に使う. 滅多に使われない。
  • require
    • Emacsにはfeatureという概念があり、それはLispシンボルで名付け,provideで宣言し, requireで読み込まれる。これを使うとファイルの読み込みが一回で済む。

ちなみに以下の式は同義になります

(require 'timestamp)
(require 'timestamp "timestamp")
  • auto-load
    • ファイルのロードをそれが必要な時になるまで遅らせる事が出来る。関数と併用される
  • eval-after-load
    • これを使うことであるコードの実行を特定のファイルがロードされるまで遅らせる事が可能。

ファイルのロードの順番は以下の通り.例えば(require 'test)は以下のような順番で検索され,実行される。test.elcsがあった場合はtest.elは読み込まれない。

  • test.elc→test.el→test
  • ファイルのロードをする際には.el,.elcなどという拡張子はなるべく付けないこと

6章 リスト

List処理. 特筆することは無いのでスルー



7 マイナーモード

  • refill-modeを例としたマイナーモードの作成を行います。
    • refill.elというものを作成する。(refill.elってEmacsに標準搭載されていたりしますが。。。まあ、昔はなかったんじゃないかと)
  • メジャーモードとマイナーモードは併用される(C-h mを実行した時にTopに出てくるEnabled minor modesがそれに他ならない)。マイナーモードには例えば以下のようなものがある。無意識の内に使ってるもの多し。
    • auto-save-mode
    • font-lock-mode
    • line-number-mode
  • マイナーモードに必要な構成要素は(defvar refill-mode nil)と(defun refill-mode ...)と (defun refill ...)の3つのみだったりします。あとはminor-mode-alistにrefill-modeを追加するという作業が必要になります。
    • この章ではマイナーモードの作り方の流れを始めに説明し、(defun refill...)を作っては改良し作っては改良しという作業を行います。
  • 始めに簡単な流れを見てみます。
  • 名前の作成
    • refill-modeという名の変数を定義し、それをバッファローカルにする
(defvar refill-mode nil
  "Mode variable for refill minor mode.")
(make-variable-buffer-local 'refill-mode)
  • refill-modeの作成
(defun refill-mode (&optional arg)
  "Refill minor mode."
  (interactive "P")
  (setq refill-mode
	(if (null arg))
	(not (refill-mode)
	     (> (prefix-numeric-value arg) 0)))
  (make-local-hook 'after-change-functions)
  (if refill-mode
      (add-hook 'after-change-functions 'refill)
    (remove-hook 'after-change-functions 'refill)))
  • refill関数の作成
(defun refill (start end len)
  "After a text change, refill the current paragraph."
  (let ((left (if (zerop len)
		  start
		(save-excursion
		  (goto-char start)
		  (end-of-line 0)
		  (point))))
	(save-excursion
	  (fill-region left end nil nil t)))))
  • minor-mode-alistにrefill-modeを追加
    • '(refill-mode " Refill")におけるRefillはモードラインに表示する短い文のことです。空白で始める必要があります。
(if (not (assq 'refill-mode minor-mode-alist))
    (setq minor-mode-alist
	  (cons '(refill-mode " Refill")
		minor-mode-alist)))
  • refillをprovideしてrefill.elの作成終了
(provide 'refill)
  • 基本的には上記の流れとなります。あとはrefill関数を修正するという作業を続ける感じですね。
  • 途中skipしてlast verのrefill関数とrefill関数内で必要となる関数の実装部分を載っけておきます。
  • skip-chars-forwardは便利ですとか
(defun before-2nd-word-p (pos)
  "Does Pos lie before the second word on the line?"
  (save-excursion
    (goto-char pos)
    (beginning-of-line)
    (skip-syntax-forward (concat "^ "
				 (char-to-string
				  (char-syntax ?\n))))
    (skip-syntax-forward " ")
    (< pos (point))))

(defun same-line-p (start end)
  "Are START and END on the same line?"
  (save-excursion
    (goto-char start)
    (end-of-line)
    (<= (point))))

(defun short-line-p (pos)
  "Does line containing POS stya within 'fill-column?"
  (save-excursion
    (goto-char pos)
    (end-of-line)
    (<= (current-column) fill-column)))

;; ver last refill

最終的なrefill関数は以下のような感じになりまする。 

(defun refill (start end len)
  "After a text change, refill the current paragraph."
  (let ((left (if (or (zerop len)
		      (not (before-2nd-word-p start)))
		  start
		(save-excursion
		  (max (progn
			 (goto-char start)
			 (end-of-line 0)
			 (point))
		       (progn
			 (goto-char start)
			 (backward-paragraph 1)
			 (point)))))))
    (if (or (and (zerop len)
		 (same-line-p start end)
		 (short-line-p end))
	    (and (eq (char-syntax (preceding-char))
		     ?\ )
		 (looking-at "\\s *$")))
	nil
      (save-excursion
	(fill-region left end nil nil t)))))
	


まあマイナーモードの流れとちょっとしたtipsの紹介, それに関数の修正作業の流れを見るという感じの内容ですね。

8章 評価とエラー処理

7章の内容を踏まえsave-excursionを改良するという章。マクロ解説。マクロを利用したlimited-save-excursionの実装+limited.elの作成という内容

マクロ知っていれば前半の内容はスルーしても可能。最終的には以下のようなlimited.elが出来上がっている。 後は7章におけるsave-excursionを(require 'limited)を実行しlimited-excursionに変更する事でrefill-modeが完成する

ちなみにlimited-save-excursionマクロに出てくるunwind-protectの意味は途中で例外処理がおきても(goto-char ,orig-point-symbol)は実行されるという意味。

>|lisp||
;; limited.el
(defmacro limited-save-excursion (&rest subexprs)
"Like save-excursion, but only restores point."
(let ((orig-point-symbol (make-symbol "orig-point")))
`(let ((,orig-point-symbol (point-marker)))
(unwind-protect
(progn ,@subexprs)
(goto-char ,orig-point-symbol)))))
(provide 'limited)
|

  • .emacsに以下の様な内容を加えておけばrefillもrefill-modeが起動するまでロードされなくなるとの事

(autoload 'refill-mocde "refill" "Refill minor mode." t)

9章 メジャーモード

quip-modeというものを作る。。メジャーモードの構造の説明 + ナローイングの説明 + 派生モードの説明(derived !!)といった感じですね。

  • quip-modeというのは名言を編集するためのモードのこと

メジャーモードの構造は以下のような感じです。とりあえずquip-modeを作る事を例とします。

  • メジャーモードの作り方の流れ
    • 名前を考える
    • quip.elというファイルを作る
    • quip-mode-hookという名前の変数の定義
    • 必要なら固有のキーマップを定義する
    • 必要ならシンタックステーブルを定義する
    • 必要ならモードの略称テーブルを定義する
    • quip-modeというコマンドの定義をする
      • kill-all-local-variablesで全てのバッファローカル変数を取り除く
      • メジャーモードをqup-modeにする
      • モードを表す文字列をmode-nameに設定する
      • 必要ならモード固有のキーマップをインストールする
      • ユーザーのフック関数を実行する
    • quipをprovideする

概要は以下のような感じです. とりあえず始めは最低限の設定にしておきます。

(defvar quip-mode-hook nil
  "*List of functions to call when entering Quip mode.")

(defun quip-mode()
  "Major mode for editing Quip files."
  (interactive)
  (kill-all-local-variables)
  (setq major-mode 'quip-mode)
  (setq mode-name "Quip")
  (run-hook 'quip-mode-hook))

(provide 'quip)

次にquip-modeにおける段落の定義を行います。

(make-local-variable 'paragraph-start)
(make-local-variable 'paragraph-separate)
(make-local-variable 'page-delimiter)
(setq paragraph-start "%%\\|[ \t\n^L]")
(setq paragraph-separate "%%$\\|[ \t\n^L]*$")
(setq page-delimiter "^%%$")
  • 次にキーマップの設定を行います。
    • メジャーモードにおけるキー設定をする際に,あまり設定するものがない場合にはmake-sparse-keymapを利用します。
    • 完全なキーマップが必要な場合にはmake-keymapを利用します。make-keymapはニダース以上の設定がある場合に於いて使うべしとのこと。

始めにキーマップの設定を行う前にQuipモード用にコマンドをdefaliasします。

(defalias 'backward-quip 'backward-page)
(defalias 'forward-quip 'forward-page)
(defalias 'narrow-to-quip 'narrow-to-page)
(defalias 'what-quip 'what-page)

次にキーマップの設定。感じとしては以下の通り

(if quip-mode-map
    nil
  (setq quip-mode-map (make-sparse-keymap))
  (define-key quip-mode "\C-x[" 'backward-quip)
  (define-key quip-mode "\C-x]" 'forward-quip)
  (define-key quip-mode "\C-xnq" 'narrow-to-quip))
  • 存在するquipの数を数えるためのコマンドの定義
    • ここではナローイングについて考える必要があります。ナローイングとはバッファの編集などをする際に一部のバッファのみに特化して表示させたいときに使います。
    • quipの数を数えるためには現在のバッファがナローイングされている場合にも対応したコードを書く必要があります。

以下は結構頻繁に使われるコード。やっている事は現在のバッファのナローイングの状態を覚えておき、(widen)で全体バッファにアクセス出来るようにした後、再度元のナローイングの状態に戻すと言ったことをしています。

(save-restriction
  (widen)
  ...)
  • コードの実装は以下のような感じになります
(defun count-quips()
  "Count the quips in the buffer."
  (interactive)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (count-matches "^%%$"))))
  • とまあここまで色々と実装してきた訳ですが、quip-modeは実際の所text-modeに+α処理を付け加えた感じなのですが完全にtext-modeの内容を踏襲するためにはsyntax-tableなどをcopyする必要があります
  • define-derived-modeという既に定義されたモードから新しいモードを派生する事ができるパッケージがある(derivedパッケージ)!! これを使う事により、text-modeの内容を維持したままquip-modeを作ることができます。内容は以下の通り

quip.el(derivedパッケージを使った例)

(require 'derived)

(define-derived-mode quip-mode text-mode "Quip"
  "Major mode for editing Quip files.
Special commands:
\\quip-mode-map"
  (make-local-variable 'paragraph-separate)
  (make-local-variable 'paragraph-start)
  (make-local-variable 'page-delimiter)
  (setq paragraph-start "%%\\|[ \t\n\^L]")
  (setq paragraph-separate "%%\\|[ \t\^L]*$")
  (setq page-delimiter "^%%$"))

;; defalias parts

(defalias 'backward-quip 'backward-page)
(defalias 'forward-quip 'forward-page)
(defalias 'narrow-to-quip 'narrow-to-page)
(defalias 'what-quip 'what-page)

;; function definition parts
(defun count-quips()
  "Count the quips in the buffer."
  (interactive)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (count-matches "^%%$"))))

;; mapping parts

(if quip-mode-map
    nil
  (setq quip-mode-map (make-sparse-keymap)))
  (define-key quip-mode-map "\C-x[" 'backward-quip)
  (define-key quip-mode-map "\C-x]" 'forward-quip)
  (define-key quip-mode-map "\C-xnq" 'narrow-to-quip)
  (define-key quip-mode-map "\C-cw" 'what-quip))


(provide 'quip)

10章 総合的な例

クロスワードパズルを編集するためのメジャーモードの作成

  • 9章の応用といった感じですかね。 結構長めのメジャーモードの作成 + 色々なテクニックの解説といった感じ。 マウスコマンドとか(interactive "@e")とか非同期egrepとかなんとか
  • ソース打ち込みは長いんで止めましたw まあ本にあるコードがあんま纏まっていないってのも理由になるのですが。。。


とまあこんなとこですね。なかなかいい感じの内容。 Common LispとかSchemeとか知ってるなら問題なく分かると思いますが、Lispそのものを知らない場合やや難しめの内容かもしれません。まあ内容が古いんで結構古い感じのコードなんですけど(defgroupとかないし。。)何かしら役に立つのではないかと。

SIG-KBS(知識ベースシステム研究会)のGoogle Marketingにおけるコンピューターサイエンスと統計学の講演メモ

SIG-KBSでGoogleの講演があり、それのメモを取っておいたので載せておきます。Google関連の講演はこれまで三回くらい聞いたことがあるんですけど、統計関連は聞いたことが無かったので非常に面白かったですね。

Ustとかスライド撮ってそれをwebに挙げるってのは勘弁してください><という感じだったのですが、Blogにするくらいなら良いらしいので、載せておきます。メモそのまんまなんで特に補足説明とかないですけど。。

社内専用最適化言語とか、統計解析に使っている統計モデルとかの説明は面白かったですね。

  • Computer Science and Statistics at Google Marketing
  • 2009 Quantitative Marketing Manager @ Tokyo
  • PhD in Engineering from the University of Tokyo (focused on computational science)
  • modeling, computational and simulation analysis of complex networks
  • Visualization of large-scale complex graphs!
  • 一人しかいない
  • Member
  • Web search
  • 統計
  • 1 Let others speak for you
  • 2,3 重要
  • 2 Data not hype
  • 3 Results must be trackable
  • 4 Promote trial
  • 5 YOu're smart and your time matters.
  • 6 We're serious. Except when we're not.
  • 7 Big ideas move us.
  • Seven principles of Google Marketing
  • 社内の全ての社員が結構データのアクセスができるかというと違う
  • ソースコードのアクセスはソフトエンジニアのみだけど統計関連の人も見れるとか
  • 制限厳しい
  • エンジニアでない人がlog データをどう見ればいいのか
  • レポーティングのためのツール
  • analysis skills
  • engineering skills
  • product and market-specific knowledge and expertise
  • extensive analytical and statistical skills.
  • analyses to help inform marketing strategies for key products
  • We have the same data/logs access privileges as software engineers
  • We are supposed to be data analysis professionals
  • 二つの事例
  • Display Ad Expertiments
  • 100から120に上がる
    • これだけでは分からない
  • こうか
  • Test & control ads
    • 動画の広告効果の測定は難しい
  • 比較実験
  • Media Mix
  • 統計モデル
  • how does data analysis work at Google
  • Data analysis Visualization(R, SQL, visualization SQL)
  • Python
  • SQL
  • 統計モデル
  • Statistical Analysis
  • やってる事は大学の研究と似ている
  • Rのライブラリを作っている人がGoogle社内に結構いる。
  • Data analysis procedure
  • datapull from various logs
  • datapull from other data sources
  • aggreage and process
  • statistical analysis -- apply statistical models on the data
  • visualize and publish as presentation or report
  • Logs(70%)
  • access log
  • client download/update log
  • we dont' use rdbms at this stage
  • simply data is too huge
  • requires distributed computing with many machines
  • ofen no complex data manipulation is needed
  • Goal of data analysis is ofen rather simple
    • sum histogram max min topN filtering
  • 解析手法はシンプルであったりする
  • テラバイトは一気に行く
  • RDBMSは必要ない
    • 難しいのはjoin
    • そういう事をする必要はない
  • logの構造
    • request URL
  • Sawzall
  • Query Geo Distribution
  • datum: table summ[t: time][lat :int][lon: int] of int:proto "querylog.proto"

log_record:QueryLogProto

  • 一日で習得出来る言語
  • MapReduceの処理を隠蔽して気楽に使える。
  • 社内最適化激しい
  • 90数%
  • 速い
  • Failure-obvious
    • discard and re-calcuatet the record with error rather than stall whole computation.
  • MySQL like database negine
  • Need to parse and aggreage different dat sources
    • usually write Python script
    • sometimes use local MySQL database for aggragation.
  • 統計モデルを作る
  • Apply appropriage statistical methods for given problems Some examples
  • Time-series(seasonal ARIMA) model
  • LIME mixed effects(LME)
  • Random forest models
  • DhD propensity scoring
  • Experimental design
  • 20+ statisticans and quant analysts on the team.
  • R mostly commonly used
  • 今後伸びていくであろうクライアントの解析 decision tree
  • 自己相関と移動平均で向こう何千年のどうのこうの
  • 時系列分析!!!
  • Visualization and Presentation
  • 時系列相関のアニメーション
  • Adhering to Engineering standards
  • Sharing al source codes with all other software engineers
  • check code into single repository for the whole company
    • your code may be used or edited by someone in the future
  • all codes have to follow coding styles
  • all codes have to be reviewed by peers before check in
  • Sharing computing resources with all other engineers.
    • K distributed machines.
  • same infrastructure as production.
  • 使い捨てコードもレビューされる
  • 守備範囲が広い
  • 問題は定義されていない
  • YoutubeのTraficをなんかに使えないか
  • どういったアプローチが必要か考えるところからプロジェクトスタート
  • Challenges we are facing
  • Complex questions without simple solutions
  • Large volumes of data
    • can't achieve w/o sophosticated computing infrastructure
    • analysts need to have necessary technical / engineering and quantitative skills
  • Limited resources (hiring & training)
  • Privacy
  • 社内向け統計学者は全然いない。
  • CS + Statistical + math backgraound is difficult.
  • 統計の教科書を出して勉強している。
  • Statistics専攻は日本にはあまりいない。
  • Google has many data analyst teams, including us QM
  • We are NOT software engineers but are equipped with either engineering or statistics backgrounds and adhere to engineering standards at Google
  • We undertake complex research and modeling projects that involve large-scale data processing and intensive statistical analysis.
  • We are hoping
  • 手に入るものは何でも使う方向
  • low dataがいい
  • 金額との兼ね合い

[emacs] Emacsのmulti-termの設定など

Emacsmulti-termが中々いい感じなので設定ファイルの備妄録など書いておきます。

ちなみにtermやansi-termと比較してmulti-termのいい点としては以下の点があります。

  • Emacsとのketstrokeとのバッテイングを回避
    • termやansi-termだとM-xとかstrokeがバッテイングしてるのだけどそれを回避してくれる
  • 複数のterminal bufferを作成可能。かつ、切り替えが軽快
  • term専用のdedicatedコマンドの実装

まあ感じとしてはtermの上位版という感じですかね。設定は以下のような感じにするとよいかと

(require 'multi-term)
(setq multi-term-program "/bin/zsh")

(add-hook 'term-mode-hook '(lambda ()
			     (define-key term-raw-map "\C-y" 'term-paste)
                             (define-key term-raw-map "\C-z"
                               (lookup-key (current-global-map) "\C-z"))))

(global-set-key "\C-cc" 'multi-term)
(global-set-key "\C-cn" 'multi-term-next)
(global-set-key "\C-cp" 'multi-term-prev)

設定としては(define-key term-raw-map "\C-y" 'term-paste)の部分が重要でこれを設定しておくとクリップボードからのPasteが有効になります。C-zの方はelscreenを使うための設定です。またこの場合local-set-keyだとうまくいきません。

まあそんなとこかと. 感じとしてはEmacs内でTerminalみたいな事、もしくはそれ以上の事ができる感じですかね。

[emacs][zsh][vim]CarbonEmacsでzshとかvimをまともに使う方法 

需要があるのか謎なのですが,Carbon Emacs内部でzshとかvimをまともに使う方法について書いておきます。この手法は別にCarbonEmacsじゃなくてEmacsとかでも使えるまする(Terminalで確認済み)。

とりあえず始めにEmacsで使えるshellの種類について簡単に触れておきます。 Emacsで使えるshellは大別してshell派とterm派の二派閥があります。

  • shell派はEmacs側が殆ど実装しており?,挙動が普通のshellっぽくないのが特徴です。zshはshell派でも普通に使えるんですけどこの中でvimとか実行するとおぞましい事になります。
  • term派はEmacs側で実装というよりは普段使っているshellをEmacsで使うという感じのshellです。vimを実行する場合当然こっちを選択します。当然今回もこっちに関する説明となります。

ちなみに、termにはterm,ansi-term,termの拡張版のmulti-termなど色々と種類がありますが挙動は殆ど同じっぽいです。

Emacsでterm派のshellを使うにおいて重要になる事はzsh環境変数であるTERM,TERMINFO,TERMCAPを如何にうまく取り扱うかにかかっています。

結論からいうと,TERMを変えておけば,TERMINFO,TERMCAPについてはあまり考える必要はないです。

Emacsでterm関連のコマンドを実行するとバックスペースが効かないとかEnterが効かないという現象が起こりますが、これはzsh環境変数、特にTERMINFO,TERMCAPがうまく設定されていない事が原因のようです。TERMINFOはmacだと/sw/share/terminfo/ファルダから参照されます(たぶん)。参照されるTERMINFOはzshの変数として設定しておかなくとも,TERMの種類をshellの環境変数として設定しておけば自動的に参照されます(設定してなくても勝手に設定されますが)。例えば、zshでTERM=xtermと設定されている場合には/sw/share/terminfo/x/xtermのTERMINFO情報が自動的に参照されるっぽいです。


TERMの種類は環境変数に登録されておリ,echo $TERMで確認が出きます。ちなみに,当方の環境だとTerminalだと$TERM=xterm-color, iTermは$TERM=xtermとなっていました。

さて、これがEmacsで実行されるtermの環境だとどうなるのか?というわけですが、当方の環境だとTERM=eterm-colorとでました。というわけでこのTERM=eterm-colorをTERM=xterm-colorと変更すれば,普通のTerminalと同じTERMINFO情報が引用されて端末挙動もTerminalと同じ感じになります。

というわけで.zshrcに以下のような設定を追加します。

 [[ $TERM = "eterm-color" ]] && TERM=xterm-color

この設定だけでも,EmacsからzshをTerminal.appと同じような感じで操作出来るようになると思います。


ちなみに,Terminal.appとEmacs内部のTerminalの環境変数についても触れておきますが、これは当然ながら全然違います。Emacs内部実行Terminalの環境変数にはTerminal.appにはない項目がいくらかあります。例えば以下のような項目があります。

EMACSDATA=/Applications/Emacs.app/Contents/Resources/etc
EMACSPATH=/Applications/Emacs.app/Contents/MacOS/libexec:/Applications/Emacs.app/Contents/MacOS/bin
TERMCAP=eterm-color:li#43:co#160:cl=\E[H\E[J:cd=\E[J:bs:am:xn:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:ce=\E[K:ho=\E[H:pt:al=\E[L:dl=\E[M:DL=\E[%dM:AL=\E[%dL:cs=\E[%i%d;%dr:sf=^J:dc=\E[P:DC=\E[%dP:IC=\E[%d@:im=\E[4h:ei=\E[4l:mi::so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:md=\E[1m:mr=\E[7m:me=\E[m:UP=\E[%dA:DO=\E[%dB:LE=\E[%dD:RI=\E[%dC:kl=\EOD:kd=\EOB:kr=\EOC:ku=\EOA:kN=\E[6~:kP=\E[5~:@7=\E[4~:kh=\E[1~:mk=\E[8m:cb=\E[1K:op=\E[39;49m:Co#8:pa#64:AB=\E[4%dm:AF=\E[3%dm:cr=^M:bl=^G:do=^J:le=^H:ta=^I:se=\E[27m:ue=\E24m:kb=^?:kD=^[[3~:sc=\E7:rc=\E8:r1=\Ec:
__CF_USER_TEXT_ENCODING=0x1F5:1:14
TERMINFO=/Applications/Emacs.app/Contents/Resources/etc/

ちなみに、環境変数であるTERMCAP,TERMINFOの設定についてはTERM=xterm-colorと設定した場合には/sw/share/terminfo/x/xterm-colorの情報が優先されるのでスルーされます。

次にterm内でvimを使う方法ですが、これはshellコマンドを実行した場合とは全く挙動が違い普通に使えます。注意としてはEscがEmacsのコマンドと競合したりBSが使えない?という点くらいかと思います。まあ、そこら辺はキーの割り当てがされていない部分に設定しとけばいいんだと思います。例えば以下のような感じで.vimrcに設定を追加しておけばいいかと。

imap <C-B> <Left><Del>
nmap <C-B> <Left><Del>
cmap <C-B> <Left><Del>
imap <C-f> <Esc>
nmap <C-f> <Esc>

まあこんな感じですね。これでelscreen + zsh + screen(zsh内) + vim的な事が出来るようになるので面白いんじゃないかと思いまする。

Common Lispのパッケージマネージャはclbuildが良いらしい

気がつけば半年以上放置していた。。

Common Lispのパッケージ管理はASDF-INSTALLとか使う事が多かったようなのですが、最近ではclbuildを使う事が推奨されているとの事。作者はLuke Gorrie

というのも、cffiのインストールのページにそんな事が書いていたので気がついたわけですが。

簡単な使い方としては、clbuildをどっか適当な場所で展開して、clbuildフォルダに移動した後

./clbuild install cl-ppcre

とやり、適当なパッケージをインストールしたら、clbuildフォルダに/source/cl-ppcreとか/systems/cl-ppcre.asdとかが出来るので/clbuild/systems/を.sbclrcなどに

;; path-to-clbuildはclbuildまでのパス
(push #p"/path-to-clbuild/clbuild/systems/" asdf:*central-registry*)

上記の内容を追加するとclbuildで管理されているライブラリが使用可能になります。

参考リンク

lex & yaccことはじめ

とある図書館でlex & yaccという本が廃棄処理扱いされていたので、もらってきたのですが消化していなかったので、消化をかねてlex & yaccについての調査などしてみました。

ネットでの資料としては Lex and Yacc Primerの資料が秀逸で、とりあえずここに書いている事を理解すれば最低限lex & yaccを使えるようになるかと思います。

lex & yacc において、言語処理を作るにあたりどっちが重要かという事になるとやはりyaccの方が重要!! というのはlexってのは字句解析なので頑張れば自分でも構築可能だからです。yacc構文解析も頑張れば無理じゃないかもしれませんがきついと思います。

とっつきやすいのはどちらかというとlexの方ですね。lexは単体でも字句解析ができたりするので、lex単体でもちょっとした事はできるのですが、yaccの場合は処理内容は構文解析であるので、字句解析の処理はしないので外部プログラムが頼りになるからです。まぁyaccを使う際には字句解析も必要となるという訳なのですよ。というわけで、yaccを使う際にはlexとか自作?字句解析を使う必要があるのですよね。

実際の言語処理だとyacc用のファイルはあるけれどもlex用のは無いというパターンが多い?感じがしますね、はい。 というわけでyacc重要。 LR LR !!!

とりあえずlexからいじってみる。

lexのプログラムは単純で、正規表現を利用して字句解析をするだけです。

以下は簡単なサンプル

%{                                                                                                                                                         
#include <stdio.h>                                                                                                                                         
%}                                                                                                                                                         
                                                                                                                                                           
%%                                                                                                                                                         
                                                                                                                                                           
tukasa  printf("barusamikosu~~\n");                                                                                                                        
kagami  printf("kagaminmin\n");;                                                                                                                           
konata  printf("konatanntann\n");                                                                                                                          
                                                                                                                                                           
%%   

で、このファイルをtest.lとして保存し以下のようにコンパイルすると

gcc -o test test.l -ll

testバイナリができるので、字句解析が出来ているか調べる. tukasa, kagami, konataといった感じで入力すると以下のような感じになるはず

./test

tukasa
barusamikosu~~

kagami
kagaminmin

konata
konatanntann

とりあえずlexの最低限度の動作が確認できたら次はyaccとの連携を行う。

yaccとlexとの連携に関してlexで行う事はというと、yaccで定義したトークンというものをlex内部で字句解析を行い返して、yaccではその返り値(トークン)をもとにyacc内部で定義した文法定義を元に色々と処理をしていく!!! という感じ。 

というわけでlex内部では、yaccで定義した定義を参考するためにyaccファイルをyaccコマンドを行った際に現れる、y.tab.hをインクルードする必要があるという訳なんですよね。主従関係的に考えるとyaccの方が上的なポジションといってよさそうですね。

一方、yaccはというと、こっちは文法を定義して構文解析を行うといます。

とりあえず簡単なサンプルソースを作ったので掲載など

とりあえずファイル名はtete.l tete.y とする


tete.l

%{                                                                                                                                                         
#include <stdio.h>                                                                                                                                         
#include "y.tab.h"                                                                                                                                         
%}                                                                                                                                                         
                                                                                                                                                           
                                                                                                                                                           
%%                                                                                                                                                         
                                                                                                                                                           
tukasa  return TUKASA;                                                                                                                                     
kagami  return KAGAMI;                                                                                                                                     
konata  return KONATA;                                                                                                                                     
[0-9]+  yylval=atoi(yytext); return NUMBER;                                                                                                                
                                                                                                                                                           
%% 

ちなみにreturnをかえしているNUMBERとかはyaccファイル内部で定義しています

tete.y

%{                                                                                                                                                         
#include <stdio.h>                                                                                                                                         
#include <string.h>                                                                                                                                        
                                                                                                                                                           
void yyerror(const char *str)                                                                                                                              
{                                                                                                                                                          
    fprintf(stderr,"error: %s\n",str);                                                                                                                     
}                                                                                                                                                          
                                                                                                                                                           
int yywrap()                                                                                                                                               
{                                                                                                                                                          
    return 1;                                                                                                                                              
}                                                                                                                                                          
                                                                                                                                                           
main()                                                                                                                                                     
{                                                                                                                                                          
    yyparse();                                                                                                                                             
}                                                                                                                                                          
                                                                                                                                                           
%}                                                                                                                                                         
                                                                                                                                                           
%token TUKASA KAGAMI KONATA NUMBER                                                                                                                         
                                                                                                                                                           
%%                                                                                                                                                         
                                                                                                                                                           
commands:                                                                                                                                                  
    | commands command                                                                                                                                     
    ;                                                                                                                                                      
                                                                                                                                                           
                                                                                                                                                           
command:                                                                                                                                                   
    TUKASA NUMBER                                                                                                                                          
    {                                                                                                                                                      
        printf("\tTUKASA is %d\n",$2);                                                                                                                     
    }                                                                                                                                                      
    ;                                                                                                                                                      

とまぁこんな感じでとりあえず作成しています。yaccの文法定義はTUKASA NUMBERだけなので、この文法にあえばTUKASA is %d(なんか数字) が出力されます。

とりあえずコンパイル

yacc -d tete.y
flex tete.l
gcc -o tete y.tab.c lex.yy.c -ll

で、バイナリを実行する. テストとしてtukasa 1, tukasa 3, tukasa 33などと入力してみる

./tete

tukasa 1
TUKASA is 1

tukasa 3
TUKASA is 3

tukasa 33
TUKASA is 33


とまぁこんな感じで出力されるはずです。これで最低限度lex & yaccの連携ができたので後はこれを拡張すれば簡単な言語処理とかできるかも!! というわけですね。

参考リンク: