deform

let* let labels macrolet は使用頻度が高く、組み合わせて使うことも多い。すると、必然的に入れ子になり、自然にインデントを行うとインデント量が大きくなる。これは見づらいということで、入れ子を解消する書き方を考えてみた。


これらの関数(マクロ、あるいは制御構造)はどれも似た形をしている。

(FN ({NAME VALUE}*) FORM*)

(FN1 ({NAME VALUE}*) FORM*
    (FN2 ({NAME VALUE}*) FORM*))

入れ子にして用いるときは、大体親のFORM*の最後尾に子の関数をおけばいい。だから、全て同列に記述して、あとで望む順番に振り分ければ構造が作れる。

;こういう風に記述する。
(form (fn1-hoge)
      (fn1-foo)
      (fn1/form-bar)
      (fn2-buzz)
      (fn2/form-fizz))
;↓変形
(fn1 ((fn1-hoge)
      (fn1-foo))
  (fn1/form-bar)
  (fn2 ((fn2-buzz))
    (fn2/form-fizz)))

このような変形をすると構造が見えにくくなり、可読性が減るので勧めはしない。こういう書き方もできるということを試してみるだけである。


さて、何の手がかりもないのでは、構造の振り分けが行えない。そこで、束縛される変数や関数にラベルを貼り、それに従って振り分けることにする。宣言部は、ラベル単体を付与することで表す。実際には次のような変換になる。

(let ((x1 ~)
      (x2 ~))
  (hoge)
  (fuga)
  (flet ((y1 ~)
         (y2 ~))
    (foo)
    (bar)))

;↑元の構造
;↓変換後の構造

(form (x let y flet)
  ((x1 ~)
   (y1 ~)
   (y (foo))
   (x2 ~)
   (y2 ~)
   (y (bar))
   (x (hoge)
      (fuga))))

上の例はわざと適当に書いたが、1つずつ見て振り分けるため、構造間の順番をばらばらに記述できる。関連するfletとmacroletを近くに書いても問題ない。ただし、構造内の順番は明示しないため、実行順に書かなければならない。ラベルがついているため、一様に書いてもそれほどは見づらくはならないだろう。また、宣言全体を囲っているのはラベル宣言と区別したり、拡張するときの利便性のためである。


この仕様を満たすマクロを以下に示す。

(defmacro form (prefix-list body)
  (let* ((l (group prefix-list 2))
         (lst (mapcar (lambda (x) (list (car x) (cadr x) nil nil)) l))
         (pat (stable-sort (reverse (mapcar (lambda (x) (list (symbol-name (car x)) (car x) (cadr x))) l))
                           (lambda (x y) (> (length x) (length y))) :key 'car)))
    (dolist (arg body)
      (multiple-value-bind (place class)
          (form-classify (symbol-name (car arg)) pat)
        (if class
            (if place (push (cdr arg) (cadddr (assoc (car class) lst)))
              (push arg (caddr (assoc (car class) lst)))))))
    `,(funcall (labels ((rec (lst)
                          (if (cdr lst)
                              (append (f (car lst)) (list (rec (cdr lst))))
                            (f (car lst))))
                        (f (x) (f-sub-1 (cdr x)))
                        (f-sub-1 (x) (nconc (list (car x)) (list (nreverse (cadr x))) (f-sub-2 (caddr x))))
                        (f-sub-2 (x) (apply 'nconc (nreverse x)))) #'rec)
               lst)))

(defun form-classify (name pat)
  (dolist (p pat (values nil nil))
    (if (string= name (car p)) (return (values t (cdr p)))
      (let ((len (length (car p))))
        (if (and (< len (length name))
                 (string= name (car p) :end1 len)) (return (values nil (cdr p))))))))

(defmacro deff (name lambda &optional doc)
  `(progn
     ,@(if doc
           `((setf (get ',name 'lisp::function-documentation) ,doc)))
     (setf (symbol-function ',name)
           (si:*set-function-name
            ,lambda ',name))))

(defmacro deform (name prefix-list body &optional doc)
  `(deff ,name
         (form ,prefix-list ,body)
         ,doc))

このdeffはこのフォームで関数を返す場合、それを直接関数として定義するためのものである。関数が入れば何でも良いので、lambdaなどでも使える。そして、formとdeffを組合せたものがdeformである。


このマクロを用いて、もっと便利なマーカが欲しい。 - 象徴ヶ淵の拡張を行ってみた。その結果がこれである。

(deform embed-register (/ macrolet ~ labels)
  ((/new-marker () `(set-marker (make-marker)))
   (/new-text () `(cons 'text (~copy-region)))
   (~copy-region ()
     (/region buffer-substring))
   (/new-rectangle () `(cons 'rectangle (~copy-rectangle)))
   (~copy-rectangle ()
     (let ((buffer *rectangle-kill-buffer*))
       (/region copy-rectangle)
       (rotatef *rectangle-kill-buffer* buffer)
       buffer))
   (/new-window-configurationon () `(current-window-configuration))
   (/new-keyword-hash-table () `(cons 'keyword-hash-table keyword-hash-table))
   (/new-regexp-keyword-list () `(list 'regexp-keyword-list regexp-keyword-list (syntax-table)))
   (/get () `(gethash n *register-table*))
   (/set (arg) `(setf (/get) ,arg))
   (/call () `(call-interactively command))
   (/command (&rest args)
     `(lambda (&optional n)
        (interactive "p")
        (if n (progn ,@args)
          (/call))))
   (/use (&optional cur)
     `(let* ((arg (/get))
             (class (~chk arg))
             ,@(if cur `((new ,cur))))
        (/use-case ,(if cur t nil))))
   (/region (foo) `(,foo (region-beginning) (region-end)))
   (~yank-rectangle (arg)
     (rotatef arg *rectangle-kill-buffer*)
     (yank-rectangle)
     (rotatef arg *rectangle-kill-buffer*))
   (/use-case (&optional s)
     `(case class
        (marker
         (and (set-buffer (marker-buffer arg))
              (goto-marker arg)
              (recenter)
              ,@(if s '((/set new)))
              ,(/use-msg "マーカ")))
        (text
         ,@(if s '((/region delete-region)))
         (insert (cdr arg))
         ,@(if s '((/set new)))
         ,(/use-msg "リージョンテキスト"))
        (rectangle
         ,@(if s '((/region delete-rectangle)
                   (goto-char (region-beginning))))
         (~yank-rectangle (cdr arg))
         ,@(if s '((/set new)))
         ,(/use-msg "矩形"))
        (window-configuration
         (set-window-configuration arg)
         ,@(if s '((/set new)))
         ,(/use-msg "ウィンドウ状態"))
        (keyword-hash-table
         (make-local-variable 'keyword-hash-table)
         (setq keyword-hash-table (cdr arg))
         ,@(if s '((/set new)))
         ,(/use-msg "キーワード"))
        (regexp-keyword-list
         (make-local-variable 'regexp-keyword-list)
         (setq regexp-keyword-list (cadr arg))
         (use-syntax-table (caddr arg))
         ,@(if s '((/set new)))
         ,(/use-msg "正規表現キーワード&シンタックス"))
        (t (message "そのスロットは使ってないよ。") nil)))
   (/use-msg (msg) `(if s '(message "~A入れ替え" ,msg)
                      '(message "~A適用" ,msg)))
   (/cur-case ()
     `(case class
        (marker (/new-marker))
        (text (/new-text))
        (rectangle (/new-rectangle))
        (window-configuration (/new-window-configurationon))
        (keyword-hash-table (/new-keyword-hash-table))
        (regexp-keyword-list (/new-regexp-keyword-list))))
   (/mem (new msg) `(/command (/set ,new) (message ,msg)))
   (~chk (arg)
     (if (markerp arg) 'marker
       (if (consp arg) (car arg))))
   (~gen (direction &optional command)
     (case direction
       (syntax (/mem (/new-regexp-keyword-list) "正規表現キーワード&シンタックスコピー"))
       (keyword (/mem (/new-keyword-hash-table) "キーワードコピー"))
       (window (/mem (/new-window-configurationon) "ウィンドウ状態コピー"))
       (rectangle (/mem (/new-rectangle) "矩形コピー"))
       (region (/mem (/new-text) "リージョンコピー"))
       (marker (/mem (/new-marker) "マーカ設定"))
       (apply (/command (/use)))
       (swap (/command (/use (/cur-case))))))
   (~ #'~gen)))

(defvar *register-table* (make-hash-table :size 7))

追記2011-02-05 22:41

embed-registerが間違ってたのでこっそり修正。

使い方は、

(global-set-key #\NUL (embed-register 'marker 'set-mark-command))
(global-set-key #\C-f (embed-register 'apply 'forward-char))

などと書く。Universal argument を与えて C-SPC を入力すると、普通にマーカを設定するのではなく、argの番号にマーカを記憶させる。これを実際のマーカに適用するときは同じ Universal argument を与えて C-f を入力する。というように、embed-registerのキーワードと元のコマンドを使ってコマンドを登録する。元のコマンドのarg指定動作は出来なくなるので注意。使えるキーワードと動作は~genを見れば大体分かると思う。