Hatena::Groupcadr

kozima の日記

2010-12-16

map vs. loop

| 00:11

xyzzy の session.l の一部を elisp で書いてみたのですが,そのときに感じたこと。どうも複雑な処理は無名関数にして map するより loop で書いたほうが短いし楽だし読みやすい(気がする)。

ということで具体的にコードを並べてみることにしました。

session.l は,開いていたファイルの情報やバッファのポイント位置,ウィンドウの状態などをファイルに書き出しておいて後で復元する機能を提供しています。その中から,現在開いているファイルの情報を集める部分と,それを復元する部分のコードを比べてみます。

まず xyzzy のソースから。

(defun list-buffer-info ()
  (save-excursion
    (let ((info nil))
      (mapc #'(lambda (buffer)
                (when (file-visited-p buffer)
                  (set-buffer buffer)
                  (push (list (get-buffer-file-name buffer)
                              (point)
                              (cons buffer-mode
                                    (mapcan #'(lambda (mode)
                                                (let ((var (and (consp mode) (car mode))))
                                                  (and (symbolp var)
                                                       (boundp var)
                                                       (symbol-value var)
                                                       (list var))))
                                            *minor-mode-alist*))
                              (mapcar #'(lambda (var)
                                          (and (symbolp var)
                                               (local-variable-p var)
                                               (cons var (symbol-value var))))
                                      *buffer-info-variable-list*))
                        info)))
            (buffer-list :buffer-bar-order t))
      (nreverse info))))

(defun restore-buffer-info (info)
  (let ((obuffer (selected-buffer)))
    (mapc #'(lambda (i)
              (let ((file (pop i)))
                (when (file-exist-p file)
                  (handler-case
                      (let ((point (pop i))
                            (mode (pop i))
                            (minor nil))
                        (when (listp mode)
                          (setq minor (cdr mode))
                          (setq mode (car mode)))
                        (let ((*find-file-auto-mode-function* mode))
                          (find-file file))
                        (goto-char point)
                        (mapc #'(lambda (f) (and (fboundp f) (funcall f))) minor)
                        (mapc #'(lambda (x)
                                  (when (and (car x) (symbolp (car x)))
                                    (make-local-variable (car x))
                                    (set (car x) (cdr x))))
                              (pop i)))
                    (file-error (c)
                      (si:*print-condition c))))))
          info)
    (set-buffer obuffer)))

エラーチェックの部分を削ってますが,基本的に同じことをやる elisp のコード。ついでにちょっとコメント入り。

(defun mylib-session-list-buffer-info ()
  (loop for buffer in (buffer-list)
        as file = (buffer-file-name buffer)
        if file collect
        (with-current-buffer buffer
          (list file
                (point)
                ;; collect minor mode info
                (loop for mode in minor-mode-alist
                      as var = (and (consp mode) (car mode))
                      if (and (symbolp var) (boundp var) (symbol-value var))
                      collect var into vars
                      finally (return (cons major-mode vars)))
                ;; collect local variable info
                (loop for var in mylib-session-buffer-info-variable-list
                      if (and (symbolp var) (local-variable-p var))
                      collect (cons var (symbol-value var)))))))

(defun mylib-session-restore-buffer-info (info)
  (save-excursion
    (loop for (file point (mode . minor) local-vars) in info do
          (if (file-exists-p file)
              (ignore-errors
                ;; open visited file,
                ;; (find-file-noselect file)
                (find-file file)   ; 修正 (2010/12/17)
                (when (and (fboundp mode) (not (eq mode major-mode)))
                  (funcall mode))
                ;; move point to the original place,
                (goto-char point)
                ;; restore minor modes,
                (dolist (f minor) (if (fboundp f) (funcall f)))
                ;; and local variables
                (loop for (var value) in local-vars do
                      (make-local-variable var)
                      (set var value)))
            (message "file `%s' does not exist!" file)
            (sit-for 0.5)))))

map 系は無名関数(特に大きめのもの)と一緒に使うとあまり読みやすくない,ということは以前から思っていましたが,それが顕著に表れた気がします。たぶん,第二引数が離れてしまうせいで「何に対して map するのか」がぱっと見てわかりづらいことが,読みにくさを感じさせる一因になっているのではないかと思います。

可読性ともいくらか関係しますが,コードの行数もやや多くなりがちです。lambda で無名関数を書くと,情報量のわりに場所をとる気がします。特に無名関数がよく使われる map の第一引数のようなところでは,lambda と書いてあってもテンプレート通りという感じで,それほど情報量が多くないのではないでしょうか。そういうところに場所をとるものがあると,余計にコードサイズがかさんでいるように感じられるのかもしれません。

それからもう一つありました。map に限らず,高階関数に無名関数を渡すというスタイルは,インデントが深くなりやすいですね。これは可読性にはそれほど関係しないような気もしますが,書いているときにだんだん右に寄ってくるとなんとなく窮屈に感じます。