elisp初心者がelispを触るとdolistを使いたがる

この記事は、.emacs Advent Calendar 2013 17日目の[elisp初心者がelispを触るとmapcarを使いたがる](http://qiita.com/esehara@github/items/089f04a3c7f52ec2fcf0)がたいへん役に立ったので、2年越しの礼状です。ほんとうにありがとうございました。

コメントで指摘されていたmapcやdolistなどとあわせていろいろ試したところ、dolistを使えば幸せになれることがわかりました。

## いろいろ試してみた
[elisp初心者がelispを触るとmapcarを使いたがる](http://qiita.com/esehara@github/items/089f04a3c7f52ec2fcf0)オリジナルから、設定されるキーマップリストだけを変更したのが次のコードです。

```
(defun global-key-setting (keylist)
   (global-set-key (read-kbd-macro (car keylist)) (car (cdr (cdr keylist)))))

(defun global-key-setting-list (target-list)
  (mapcar 'global-key-setting target-list))

(global-key-setting-list '(
   ("<M-return>" expand-abbrev)
   ("C-`" expand-abbrev)
   ("C-c C-c" comment-region)
   ("C-c c" compile)
   ))
```

ただ、このコードだと、1度しか行われない処理のために関数`global-key-setting`と`global-key-setting-list`が定義されてしまうのが美しくないような…そこで、[Elisp Manual > 12.6 Mapping Functions](http://www.gnu.org/software/emacs/manual/html_node/elisp/Mapping-Functions.html)をもとに悪戦苦闘しつつも、次のように修正してみました。

```
(mapcar
 (lambda (mapkeys)
   (let ((key (car mapkeys)) (func (nth 1 mapkeys)))
     (global-set-key (kbd key) func)))
  '(
    ("<M-return>" expand-abbrev)
    ("C-`" expand-abbrev)
    ("C-c C-c" comment-region)
    ("C-c c" compile)
    ))
```

さらに、オリジナル記事のコメントを見ると、次の意見が。

>結果のリストが使われないのでdolistかmapcで良い気も。

mapcの場合は、悩む必要もなく`mapcar`を`mapc`に書き換えればよろしい。

```
(mapc
 (lambda (mapkeys)
   (let ((key (car mapkeys)) (func (nth 1 mapkeys)))
     (global-set-key (kbd key) func)))
 '(
   ("<M-return>" expand-abbrev)
   ("C-`" expand-abbrev)
   ("C-c C-c" comment-region)
   ("C-c c" compile)
   ))
```

`dolist`。こちらは少し書き方が違うのか。

```
(dolist
    (mapkeys
     '(
       ("<M-return>" expand-abbrev)
       ("C-`" expand-abbrev)
       ("C-c C-c" comment-region)
       ("C-c c" compile)
       ))
  (let ((key (car mapkeys)) (func (nth 1 mapkeys)))
    (global-set-key (kbd key) func)))
```

`mapcar`と`mapc`では最後に記述していたリストを、`dolist`では先に記述します。また、`lambda`を明示する必要がありません。

私も含めてelisp初心者は`dolist`版の方が直観的にわかりやすそうな気がします。というわけで、`dolist`を使うことにしました。

## もっと実用的に

`global-set-key`のより実用的な設定を求め、次のようにしてみました。

```
(dolist
    (mapkeys
     '(
       ("<M-return>" expand-abbrev)
       ("C-`" expand-abbrev)
       ("C-c C-c" comment-region)
       ("C-c c" compile)
       ))
  (let ((key (car mapkeys)) (func (nth 1 mapkeys)))
    (if (not (functionp func))
        (message "Warning: function `%s' is NOT defined." func)
      (global-set-key (kbd key) func))))
```

新しいキー設定を追加するときはリストに`("<key>" <function>)`を追加するだけで済むようにしました。また、設定に失敗するときは警告を\*Message*バッファに出力するようにしました。

できあがったコードを眺めてみると、散らかりがちなglobal-set-keyの設定を1つのリストにまとめられるのが、実用的でかつ美しいかな、と。Lispの基本はリスト処理（List Processing）である、ということもあらためて実感させられます。

調子が出てきたので、モードマップごとのキーバインド設定も`dolist`で記述してみました。

モードマップごとのキーバインド設定では、フックを設定するのが確実です。フックの設定時は、[lambda式よりも関数を定義して使う方が望ましい](http://d.hatena.ne.jp/tomoya/20100112/1263298132)ので、キーバインド設定ごとに関数が自動的に定義されるよう、ちょっと工夫しました。

```
;; モードごとのキーバインドを設定
;; 設定時、関数 my-init-<mode-map-name>-keybind を定義し、フックに追加する
;; リストの形式: (mode-library mode-hook mode-map-name ((key1 function1) (key2 function2)))
(dolist
    (list
     '(
       ("text-mode" text-mode-hook text-mode-map
        (
         ("C-M-i" dabbrev-expand) ; ispell起動を無効にし、dabbrev-expand を設定
         ))
       ("tex-mode" latex-mode-hook latex-mode-map
        (
         ("<M-return>" latex-insert-item) ; latex-insert-itemを再設定
         ("C-c p p" exopen-buffer-pdffile)
         ("C-c p d" exopen-buffer-dvifile)
         ("C-c C-c" comment-region)     ; tex-compileを無効にし、comment-region を設定
         ))
       ("lisp-mode" emacs-lisp-mode-hook lisp-mode-shared-map
        (
         ;; ("<M-return>" noexist)      ; デバッグ用
         ("<M-return>" completion-at-point)
         ))
       ("mediawiki" mediawiki-mode-hook mediawiki-mode-map
        (
         ("C-x C-s" save-buffer)
         ))
       ))
  (let* ((lib (car list)) (hook (nth 1 list))
         (modemap (nth 2 list)) (mapkeys (nth 3 list))
         (modemap-name (symbol-name modemap))
         (func-init-keybind (read (concat "my-init-" modemap-name "-keybind"))))
    (eval-after-load lib
      (progn
        (fset
         func-init-keybind
         `(lambda ()
            (dolist
                (map ',mapkeys)
              (let ((key (car map)) (func (nth 1 map)))
                (if (not (functionp func))
                    (message
                     "Warning: In setting %s, function `%s' is not defined."
                     ,modemap-name func)
                  (define-key ,modemap (kbd key) func))))))
        `(add-hook ',hook ',func-init-keybind)))))
```

結果として、`dolist`をネストして使うことになりました。やや複雑なリストとコードになったけど、リストの編集自体はそれほどむつかしくはないかな、何年か経って編集するときでも問題ないだろう、と思っています。

init.elは、キーバインド以外の項目も`dolist`にまとめることでシンプルで編集しやすい状態を保てます。処理を重複して書く必要がないですし、多くの場合、処理を変更せずにリストを編集すれば必要な設定変更ができますので。

## まとめ
`mapcar`や`dolist`をうまく使いこなせれば、init.elをシンプルに保つことができますし、Lispの本質にもちょっと近づけた気になります。

それでは良い年末と年明けを。