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回も記述していて、感覚的には非常に見づらいんですが、こう言う厄介な仕様を含んでいる為、敢えてああ言う書き方を選んだのでしょう。多分。


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

4 件のコメント:

  1. 「Emacs Lisp」にあまり詳しいわけではないのですが……。

    「Emacs」では1つの「メジャーモード」と複数の「マイナーモード」を同時に読み込むことができます。

    様々な「マイナーモード」と併用して使う可能性がある「メジャーモード」では、その場でしか使わないような変数を大域変数としてずっと残しておくと、他の「マイナーモード」の変数と被った場合、バグの原因になってしまいます。

    そこまで考えると、jibaku は局所変数の方が妥当だと思います。

    返信削除
  2. コメントありがとうございます。

    そうですねえ。
    実はそれ気になって、cl-extentionにdefpackageみたいなのあるかどうか探しては見たのです。
    defpackageさえあれば、ソースレベルでの名前の重複は回避出来るのでは・・・?とか思ったんですけど、ザーっとWeb検索する限り、見つからなかったのです。

    何か上手い手ないのか、ちょっと考えてみたいと思います。

    返信削除
  3. 別解ですが,
    (let ((key (char-to-string (+ ?a (random 26)))))
    (define-key lune-mode-map key 'lune-jibaku))
    と書けば変数 jibaku を使わずに済むと思います。

    返信削除
  4. >kitokitokiさん

    ご提案、ありがとうございます。

    返信削除