章末には前回の記事で扱ったコードの改訂版が紹介されています。
;; -*- 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は納得しやすいでしょう。ポイントは
- 写像関数
mapcar
を用いて - 無名関数ラムダ式を
loop ~ collect
で作り出した文字コードのリストへ写像する
です。
前回ではSRFI-1のiotaみたいな機能をわざわざ書きたくない、メンド臭い、ってバッサリ切り捨てましたが(笑)、今回は文字コードのリストを使う為、
collect
キーワードを使っています。まあ、こう言う使い方をする以上は悪名高い
loop
構文も大した事ないわけですよ。むしろ、Pythonのリスト内包表記程度、には軟弱に使えます(笑)。っつーわけで、第2講章末問題のお題。
【問】前問「るねきちモード」を以下のように改良した「るねきちモードII」を作成せよ。
- a~zの押すキーに対応して「るねきちAナリ」~「るねきちZナリ」を挿入するように変更せよ。
- さらに、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 "自爆!")))
見慣れない関数等は以下の通り。
- format(関数:ANSI Common Lispのそれとちょっと違う。)
- visible-bell(ユーザ・オプション)
- ding(関数)
- 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))))
修正ポイントは以下の通りです。
自爆キーが何になるか、は大域変数として決めた方がスッキリとするのでは?
これはまあ、そのまんま、です。
オリジナルのコードで変数jibaku
をローカル変数として定義したのは、恐らく、変数名とするkey
が被らないようにする為だけではないか、と。
それだけに見えますね。let*
なんて持ち出さなくても、大域変数として*jibaku*
を定義しちゃった方が、構造的には簡単に思えます。define-keyを二度も使ってkey-mapを一々変更するのはムダ
基本構造的には、改良版のmap
を使ったヴァージョンと同じです。
ただし、束縛されるべき関数名を大域変数*jibaku*
を利用した条件分岐で分けています。それだけ。
この手の処理は一回で済ませられるのなら済ましてしまえ、って事ですね。繰り返し回数が分かっていて、副作用を利用する場合は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 "自爆!"))))Emacs Lispのding関数の動きが良く分からん
もうそのまんま、です。
そもそもvisible-bell
ってのが変数っぽい辺りが良く分からず、それが成立しているスコープの範囲内じゃないと(ding)
が動かない、と言う良く分からん仕様になってるんですね。
オリジナルのコードは同じ関数を3回も記述していて、感覚的には非常に見づらいんですが、こう言う厄介な仕様を含んでいる為、敢えてああ言う書き方を選んだのでしょう。多分。
とまあ、今回はこんなカンジですかね。
「Emacs Lisp」にあまり詳しいわけではないのですが……。
返信削除「Emacs」では1つの「メジャーモード」と複数の「マイナーモード」を同時に読み込むことができます。
様々な「マイナーモード」と併用して使う可能性がある「メジャーモード」では、その場でしか使わないような変数を大域変数としてずっと残しておくと、他の「マイナーモード」の変数と被った場合、バグの原因になってしまいます。
そこまで考えると、jibaku は局所変数の方が妥当だと思います。
コメントありがとうございます。
返信削除そうですねえ。
実はそれ気になって、cl-extentionにdefpackageみたいなのあるかどうか探しては見たのです。
defpackageさえあれば、ソースレベルでの名前の重複は回避出来るのでは・・・?とか思ったんですけど、ザーっとWeb検索する限り、見つからなかったのです。
何か上手い手ないのか、ちょっと考えてみたいと思います。
別解ですが,
返信削除(let ((key (char-to-string (+ ?a (random 26)))))
(define-key lune-mode-map key 'lune-jibaku))
と書けば変数 jibaku を使わずに済むと思います。
>kitokitokiさん
返信削除ご提案、ありがとうございます。