ラベル やさしいEmacs Lisp講座 の投稿を表示しています。 すべての投稿を表示
ラベル やさしいEmacs Lisp講座 の投稿を表示しています。 すべての投稿を表示

2010年1月16日土曜日

言語仕様の基礎を覚える

さて、やさしいEmacs‐Lisp講座の第2講、です。
章末には前回の記事で扱ったコードの改訂版が紹介されています。

;; -*- Emacs-Lisp -*-
;; るねきちモード Version 1 by りう, fixed by lune
(defvar lune-mode-map (make-keymap))
(let ((key ?a))
(while (<= key ?z)
(define-key lune-mode-map (char-to-string key) 'i-am-lune)
(setq key (+ 1 key))))

(defun lune-mode ()
"るねきちモードだよー!"
(interactive)
(setq major-mode 'lune-mode)
(setq mode-name " るねきちモード ")
(use-local-map lune-mode-map))

(defun i-am-lune ()
(interactive)
(insert " 僕るねきちナリ "))

同書には次のようなポイントが示されていました。

  • 文字指定にはアスキーでの番号を直接指定するよりは?<任意のアルファベット>と言う形で文字コードを指定した方が良い。

  • keyと言う形でいきなり変数指定をすると、それは大域変数として解釈されてしまうので名前空間を汚してしまう。letを使って局所変数として宣言した方が良い。


との事です。

ただ、上のコードはちょっと気になる点があるんですよね。
それはこの部分です。

(let ((key ?a))
(while (<= key ?z)
(define-key lune-mode-map (char-to-string key) 'i-am-lune)
(setq key (+ 1 key))))

こんなトコでむき出しのletなんて使うだろうか・・・?
いや、やってる事は分かりますし、理論的な問題はありません。要するに単に「スタイルとしての」問題なんですよね。CLerもSchemerもこんな形でむき出しの局所変数宣言なんてしないのでは・・・?
これは、恐らく、C言語とかやってきた人の「手癖」なんですよね。Cでソース内で変数宣言する場合、スタイル的にはこう言う形にならざるを得ないでしょうから。C的には分かる。そして、だからこそ、このスタイルで変数宣言を行うとネストが深くなってしまわざるを得ない辺りでLispが嫌われるのでしょう。

CLerもSchemerも同様の事をやりたい場合は、恐らく高階関数なり手続きなりを用いると思います。
それでここで注釈。
関数define-keyは本当に関数です。
つまり、冒頭にdefineなんて付いてる為、ついつい脊髄反射的に「マクロ」であるとか、「特殊形式」っぽく捉えちゃいそうになりますが、そーじゃなくって、単なる関数です。
従って、基本的に高階関数で適用されるべきものはdefine-keyです。変なカンジが拭えませんが、そう言う事です。紛らわしいですね(笑)。

(require 'cl)を用いてもうちょっとCLerなりSchemerが納得しやすいコードは以下のようになるでしょう。

;; -*- Emacs-Lisp -*-
;; cametan-mode Version 1
(require 'cl)

(defvar *cametan-mode-map* (make-keymap))

(mapcar #'(lambda (x)
(define-key *cametan-mode-map*
(char-to-string x) 'i-am-cametan))
(loop for key from ?a to ?z collect key))

(defun cametan-mode ()
"This is cametan-mode!"
(interactive)
(setf major-mode 'cametan-mode
mode-name "cametan-mode")
(use-local-map *cametan-mode-map*))

(defun i-am-cametan ()
(interactive)
(insert " I am cametan "))

剥き出しのletを使うより、こっちの方がCLerやSchemerは納得しやすいでしょう。
ポイントは

  1. 写像関数mapcarを用いて

  2. 無名関数ラムダ式を

  3. loop ~ collectで作り出した文字コードのリストへ写像する


です。
前回ではSRFI-1のiotaみたいな機能をわざわざ書きたくない、メンド臭い、ってバッサリ切り捨てましたが(笑)、今回は文字コードのリストを使う為、collectキーワードを使っています。
まあ、こう言う使い方をする以上は悪名高いloop構文も大した事ないわけですよ。むしろ、Pythonのリスト内包表記程度、には軟弱に使えます(笑)。




っつーわけで、第2講章末問題のお題。

【問】前問「るねきちモード」を以下のように改良した「るねきちモードII」を作成せよ。

  1. a~zの押すキーに対応して「るねきちAナリ」~「るねきちZナリ」を挿入するように変更せよ。

  2. さらに、a~zどれか1つのキーを押すと画面を3回フラッシュし、「自爆」というメッセージを表示して、バッファの内容を消去する機能を付け加えよ。バッファ内容を消去する関数は(erace-buffer)である。




やさしいEmacs‐Lisp講座の模範回答は以下の通りです。

;; -*- Emacs-Lisp -*-
;; るねきちモード Version 2 by lune
(defvar lune-mode-map (make-keymap) " るねきちモードのキーマップ ")
;; とりあえず a ~ z までのキーマップ
(let ((key ?a))
(while (<= key ?z)
(define-key lune-mode-map (char-to-string key) 'lune-i-am)
(setq key (+ 1 key))))

;; 乱数で自爆キーを決めるぞー
(let* ((jibaku (random 26)) ;0~25の乱数
(key (char-to-string (+ ?a jibaku))))
(define-key lune-mode-map key 'lune-jibaku))

;; ここから本体
(defun lune-mode ()
"るねきちモードだよー!"
(interactive)
(setq major-mode 'lune-mode
mode-name " るねきちモード ")
(use-local-map lune-mode-map))

(defun lune-i-am () ;パッケージ共通の接頭辞を持つように
(interactive) ;変えてみた
(insert (format " るねきち%sナリ "(this-command-keys))))

(defun lune-jibaku ()
"自爆関数"
(interactive)
(let ((visible-bell t))
(ding)
(sleep-for 1)
(ding)
(sleep-for 1)
(ding)
(sleep-for 1)
(erase-buffer)
(message "自爆!")))

見慣れない関数等は以下の通り。

  1. format(関数:ANSI Common Lispのそれとちょっと違う。)

  2. visible-bell(ユーザ・オプション)

  3. ding(関数)

  4. sleep-for(関数)


やっぱコーディングスタイルが見慣れないので、(require 'cl)組み入れて書き直してみたいと思います。
(それはそれで問題発覚。後述。)

;; -*- Emacs-Lisp -*-
;; cametan-mode Version 2
(require 'cl)

(defvar *cametan-mode-map* (make-keymap) "keymap of cametan-mode")

;; 自爆キーの設定
(defvar *jibaku* nil) ;大域変数 *jibaku* の初期値
;; setf で *jibaku* を乱数を使って再定義しなおすと
;; バッファが評価される度に値が更新される
(setf *jibaku* (+ ?a (random 26)))

;; a ~ z までdefine-key
(mapcar #'(lambda (x)
(define-key *cametan-mode-map*
(char-to-string x)
(if (char-equal x *jibaku*)
'cametan-jibaku
'cametan-i-am)))
(loop for key from ?a to ?z collect key))

;; ここから本体
(defun cametan-mode ()
"This is cametan-mode!"
(interactive)
(setf major-mode 'cametan-mode
mode-name "cametan-mode")
(use-local-map *cametan-mode-map*))

(defun cametan-i-am () ;パッケージ共通の接頭辞を持つように
(interactive) ;変更
(insert (format " I am cametan %s " (this-command-keys))))

(defun cametan-jibaku ()
"自爆関数"
(interactive)
(dotimes (n 3 (progn (erase-buffer)
(message "自爆!")))
(let ((visible-bell t))
(ding)
(sleep-for 1))))

修正ポイントは以下の通りです。

  1. 自爆キーが何になるか、は大域変数として決めた方がスッキリとするのでは?


    これはまあ、そのまんま、です。
    オリジナルのコードで変数jibakuをローカル変数として定義したのは、恐らく、変数名とするkeyが被らないようにする為だけではないか、と。
    それだけに見えますね。let*なんて持ち出さなくても、大域変数として*jibaku*を定義しちゃった方が、構造的には簡単に思えます。

  2. define-keyを二度も使ってkey-mapを一々変更するのはムダ


    基本構造的には、改良版のmapを使ったヴァージョンと同じです。
    ただし、束縛されるべき関数名を大域変数*jibaku*を利用した条件分岐で分けています。それだけ。
    この手の処理は一回で済ませられるのなら済ましてしまえ、って事ですね。

  3. 繰り返し回数が分かっていて、副作用を利用する場合はdotimesが便利


    Schemeやってると何でもかんでも再帰で書きたくなりますが、一方、例えば入出力系なんかの副作用が絡むと、意外と再帰ではコードが「汚れて」しまいます。
    適材適所の発想で、こう言う場合はdo系の構文に切り替えた方が得策だと思います。doの本体部は暗黙のprogn(あるいはbegin)がありますし、副作用系は特に無造作に放り込んでおけます。
    この例のように、繰り返し回数があらかじめ分かってる場合、dotimesが便利です。
    また、実際、これは(僕の基準では)ちょっと複雑なので、loop構文だと大掛かりになって適さないのでは、とか思います。

    ;; loop を使って書き直してみた自爆関数の例
    ;; コード的には dotimes に比べると
    ;; 大げさに見える
    (defun cametan-jibaku ()
    "自爆関数"
    (interactive)
    (loop repeat 3
    do (let ((visible-bell t))
    (ding)
    (sleep-for 1))
    finally (progn (erase-buffer)
    (message "自爆!"))))


  4. Emacs Lispのding関数の動きが良く分からん


    もうそのまんま、です。
    そもそもvisible-bellってのが変数っぽい辺りが良く分からず、それが成立しているスコープの範囲内じゃないと(ding)が動かない、と言う良く分からん仕様になってるんですね。
    オリジナルのコードは同じ関数を3回も記述していて、感覚的には非常に見づらいんですが、こう言う厄介な仕様を含んでいる為、敢えてああ言う書き方を選んだのでしょう。多分。


とまあ、今回はこんなカンジですかね。

2010年1月15日金曜日

メジャーモードを作ろう!

以前からEmacs Lispに興味があったわけですけど、やっと勉強をはじめたい、と思います。

Lispで実用的なプログラムを書くのは、識者の意見はともかくとして、非常に難しい、と言う事が分かってきました。
現時点では、一般的な感覚で言うと、

  • 実用的なプログラム=GUIでのプログラム


って事なのです。
はいそこ。逆らわない。識者、あるいはハッカーの「極端な」意見なんてどーでも良い、のです。
如何に素晴らしいプログラムが書けようと、コマンドラインのプログラムだったら

「何じゃこりゃ?」

って言われるのがオチ、です。
そして、プログラムは「広く使ってもらわなければ」意味が無いのです。

一般に、マジメなプログラミングの勉強として考えると、Emacs Lispは避けられる傾向があります。
と言うのも、現代的なLispではないので、動的スコープしか持ってません。
しかし、GUIがどーの、って話で考えてみると、既にEmacsは「膨大な」フレームワークを内包している。自分で一から作らなくて良い。
本来の「Emacs拡張用言語」の枠組みを超えて、古き良きBASIC的プログラミング言語として楽しむのなら、依然としてEmacs Lispは強力でしょう。何せ、元々、OSから浮きまくってるEmacsは、まさしく「環境」として捉えられるので、一種の仮想マシンとして考えられるから、です。
ゲームなんかでも、GUIをLispで一から組み上げるのは大変そうですが、一方、Emacs上なら実現は簡単ぽく思えます。
この際、出来たものがショボくてもそれは問題じゃあないのです。昔のApple II的なプログラミングは、Emacs上でこそ実現すべきじゃないのか。そんな事を思っている、のです。
Emacsは21世紀のApple IIになるべきだ、とさえ思っています。




と言うわけで、やさしいEmacs‐Lisp講座の章末問題を中心にしてメモって行きたいと思います。

【問】a~zのどのキーを押しても"僕るねきちナリ"が挿入される「るねきちモード」を作成せよ。

やさしいEmacs‐Lisp講座で紹介されている模範回答例は以下のものです。

;; -*- Emacs-Lisp -*-
;; るねきちモード Version 0 by ゆう
(defvar lune-mode-map (make-keymap))

(defun lune-mode ()
"るねきちモードだよー!"
(interactive)
(setq major-mode 'lune-mode)
(setq mode-name " るねきちモード ")
(setq key 97)
(while (<= key 122)
(define-key lune-mode-map (char-to-string key) 'i-am-lune)
(setq key (+ 1 key)))
(use-local-map lune-mode-map))

(defun i-am-lune ()
(interactive)
(insert " 僕るねきちなり "))

多分、Schemeやってる人は相当違和感覚えるコーディングスタイルなんですよね、これは(笑)。
最近では、S式でのプログラミング=関数型プログラミング、って解釈される傾向があるんで、かなり違和感を覚えるスタイルです。基本S式を並べただけで、やってることはC言語的な「手続き型」のプログラミングです。
ただ、いわゆるScheme型のプログラミングとは違い、ネストが浅いんで、「見やすい」とは言えますね。この辺敷居が高くないんで、Emacs Lispプログラミングを愛好する人が多いのでしょうか(一般に、関数型プログラミングスタイルを徹底すればするほど、ネストが深くなっていく傾向がアリ)。
また、結構馴染みが無い関数名も多いです。この辺はEmacsと言うテキストエディタと「密接な」関係にあるEmacsならでは、ですよね。
馴染みが無い関数をリファレンスにリンク貼っておきましょうか。

ブログって便利だな(笑)。メモ取るのにリンクするだけで済むし。
何で今までこう言うのを効率的に使わなかったのかしら。

ソースコードを見ると、どれが大域変数でどれが局所変数なのか、つい考え込んでしまいます。
でもダイナミックスコープのLispなんで関係無いんですがね(爆)。
しかし、どれが既存の変数でどれが新規に作った変数なのか分かり辛いのは確か、です。
*scratch*バッファで調べた結果、major-modemode-nameってのが既存の変数ですよね。よって、これらは特に名前自体を変更する事は出来ないでしょう。
しかしながら、mode-map関連のはいわゆる自由な大域変数なのかな?CLでは明示的な大域変数宣言では、*(スター)で変数名を挟む、と言う命名規約があります。

ところで、最近のelispファイルを見ると、冒頭で(require 'cl)と書いているケースが多い、です。
これはEmacs Lispでもうちょっと現代的なCommon Lispの機能を使える為のオマジナイです。
これを使ってお題のコードをもうちょっと現代的に書き直してみたいと思います。


;; -*- Emacs-Lisp -*-
;; cametan-mode Version 0
(require 'cl)

(defvar *cametan-mode-map* (make-keymap))

(defun cametan-mode ()
"This is cametan-mode!"
(interactive)
(setf major-mode 'cametan-mode
mode-name "cametan-mode")
(loop for key from 97 to 122
do (define-key *cametan-mode-map* (char-to-string key) 'i-am-cametan))
(use-local-map *cametan-mode-map*))

(defun i-am-cametan ()
(interactive)
(insert "I am cametan"))


大分現代的なCLっぽいコードになったと思うんですけど。

第一に、そもそも、古いLispと違って、Emacs Lispでもsetq一つで複数の変数をバインドできると思うんですけど。
複数setqを書きまくるのもアレなんで、纏めた方が良い、と思いました。
ここでは、(require 'cl)を用いて、setqの代わりにsetfを用いています。

第二に、原版のコードでは(setq key 97)としてからwhile使ってdefine-keyでキーを定義、(setq key (+ 1 key))してバインドしてますが、CLerだったらloop使って済ます気がします。
ここ、ですよね。

(loop for key from 97 to 122
do (define-key *cametan-mode-map* (char-to-string key) 'i-am-cametan))

Schemerだったら高階手続きであるmapで同様の処理をしたいトコでしょうけど、そもそもSRFI-1iota辺りが無いと辛い。最初に転写されるリストを生成しなきゃなりませんので。
また、キーに機能を保持させる以上、要請は「破壊的作用」を伴います。その要点を最初からクリアするなら、CL的な「破壊的な繰り返し」を用いた方がベターだ、と言う判断ですね。
こう言う場合、意外とCLのloopは短く書けて便利です。

バッファを評価したあと、M-x cametan-modeで動作を確認する事が出来ます。