Emacs Lisp でプリントデバッグをする (7)

2024/02/23 更新

この前以下の記事を書きましたが

https://qiita.com/javacommons/items/cead2325794b680bf022

機能を追加しましたので上げなおしました。最新版の xprint.el は以下のGitHubリポジトリからダウンロードできます。

> [https://github.com/emacs-pkg/xprint/releases/latest](https://github.com/emacs-pkg/xprint/releases/latest)

# インストール方法 (xprint.el)

とりあえず使ってみたい(または最新の変更に追随したい)方は、.emacs または init.el に以下を追加しておくと自動的にロードされます。.emacs 内で xprint 等を呼びたい場合は、呼び出す前の時点で以下のコードを記述しておいてください。

```elisp:~/.emacs or ~/.emacs.d/init.el
(unless (featurep 'get-feature)
  (defun get-feature (feature-name &optional url file-name)
    (if (featurep feature-name) t
      (unless url (setq url (format "https://github.com/emacs-pkg/%s/raw/main/%s.el"
                                    feature-name feature-name)))
      (unless file-name (setq file-name (format "%s.el" feature-name)))
      (let ((make-backup-files nil)
            (file-path (expand-file-name file-name user-emacs-directory)))
        (ignore-errors
          (url-copy-file url file-path 'ok-if-already-exists))
        (ignore-errors
          (load file-path nil 'nomessage))
        (featurep feature-name))))
  (get-feature 'get-feature))

(get-feature 'xprint)
```

straight.el でロードしたい場合は以下を .emacs または init.el に入れてください(但し git のインストールが必要です)：

```elisp:~/.emacs or ~/.emacs.d/init.el
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package '(xprint :type git :host github :repo "emacs-pkg/xprint"))
(require 'xprint)
```

# 機能追加1

`defmacro xformat (&rest list)`

```
(cl-loop
 for x from 1
 while (<= x 10)
 do (xformat "%02d" x))

;;; 01
;;; 02
;;; 03
;;; 04
;;; 05
;;; 06
;;; 07
;;; 08
;;; 09
;;; 10
```

# 機能追加2

`defun xpp (x)`

S式をプリティプリントします。

```
(xpp '(defun xpand-macro-scan (form callback data) (cond ((symbolp form) (funcall callback form data)) ((consp form) (cons (xpand-macro-scan (car form) callback data) (xpand-macro-scan (cdr form) callback data))) (t form))))

    |
    |
    v

(defun xpand-macro-scan (form callback data)
  (cond ((symbolp form)
         (funcall callback form data))
        ((consp form)
         (cons (xpand-macro-scan (car form) callback data)
               (xpand-macro-scan (cdr form) callback data)))
        (t form)))
```

# 機能追加3

`defun xpand-macro (form)`
`defmacro xpand (form)`

マクロを展開します。内部的に macroexpand-all を使っていますが、後処理として「symbol-name」は同じだが別アドレスのシンボルに連番を振る処理を追加しています。

例えば：

```elisp
(cl-loop for x in '(a b c d e)
         for y in '(1 2 3 4 5)
         collect (list x y))
```

を実行すると：

```elisp:結果①
((a 1) (b 2) (c 3) (d 4) (e 5))
```

が返ってきますが：

```elisp
(xpp (macroexpand-all
      '(cl-loop for x in '(a b c d e)
                for y in '(1 2 3 4 5)
                collect (list x y))))
```

でプリントされる：

```elisp
(let* ((--cl-var-- '(a b c d e))
       (x nil)
       (--cl-var-- '(1 2 3 4 5))
       (y nil)
       (--cl-var-- nil))
  (while (and (consp --cl-var--)
              (progn
                (setq x (car --cl-var--))
                (consp --cl-var--)))
    (setq y (car --cl-var--))
    (setq --cl-var-- (cons (list x y) --cl-var--))
    (setq --cl-var-- (cdr --cl-var--))
    (setq --cl-var-- (cdr --cl-var--)))
  (nreverse --cl-var--))
```

をそのまま実行すると：

```elisp:結果②
nil
```

となり、結果①と異なっています。
これは、印字上「--cl-var--」となっている変数が実は３つあるからなのです。
印字して再読み込みすると、一つのシンボル(--cl-var--)に統合されてしまうからです。

そこで xpand-macro(または、xpand)の登場です：

```elisp
(xpand-macro
 '(cl-loop for x in '(a b c d e)
           for y in '(1 2 3 4 5)
           collect (list x y)))

;;; または ;;;

(xpand
 (cl-loop for x in '(a b c d e)
          for y in '(1 2 3 4 5)
          collect (list x y)))
```

とすると `*xprint*` バッファに以下のように表示されます：

```
;;; Expanding Macro:
(cl-loop for x in '(a b c d e) for y in '(1 2 3 4 5) collect (list x y))
    |
    |
    v
(let* ((--cl-var--_1 '(a b c d e))
       (x nil)
       (--cl-var--_2 '(1 2 3 4 5))
       (y nil)
       (--cl-var--_3 nil))
  (while (and (consp --cl-var--_1)
              (progn
                (setq x (car --cl-var--_1))
                (consp --cl-var--_2)))
    (setq y (car --cl-var--_2))
    (setq --cl-var--_3 (cons (list x y) --cl-var--_3))
    (setq --cl-var--_1 (cdr --cl-var--_1))
    (setq --cl-var--_2 (cdr --cl-var--_2)))
  (nreverse --cl-var--_3))
```

`*xprint*` バッファは lisp-interaction-mode になっているので、`*xprint*`バッファの最後に移動して Ctrl-J を押すと、結果①となります。

# 従来機能

## xprint/xdump

xprint, xdump が呼び出されると自動的に `*xprint*` バッファが表示されてそこにデバッグしたい値や変数を表示させることができます。
xprint は評価後の値のみ表示されるのに対して、xdump は「変数の名前 := 値」という風に表示されます。

```
(xprint 1 (+ 2 3)) ==> 1 5

(progn
  (setq a 123 b "abc xyz")
  (xprint "label-1" a b (list a b))
  (xdump "label-2" a b (list a b))
  )
⇓
"label-1" 123 "abc xyz" (123 "abc xyz")
"label-2" a := 123 b := "abc xyz" (list a b) := (123 "abc xyz")
```

以下に xprint と xdump の出力の違いをデモした際のスクリーンショットを示します：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140610/0869ca77-c8b9-effe-204a-520880eba7bb.png)

## xclear

```
(xclear) ==> *xprint* バッファをクリアします。
```

xclear 関数はプログラムの任意の位置から呼び出せます。interactive指定付きなので、M-x xclear でも呼び出せます。global-set-key してもよいでしょう。
(Windowsの場合は、Alt-x x c ENTER で呼び出せます)

## xsleep

指定したミリ秒間実行を停止(スリープ)します。(また強制的にEmacsの画面再描画を促します。)
また引数にnilを渡した場合には何も行いません。(xsleep文自体が無効になります)

`1.` Emacs は処理するGUIイベントが空になるまで画面の再描画をしませんので、xprint/xdump を呼んですぐに(リアルタイムで)表示させたい場合には、

```
(xprint 123)
(xsleep 0)
```

のように xsleep を呼ぶ必要があります。

`2.` 表示がながれてしまうので、ディレイを入れたい場合にもxsleepは使えます。(指定がミリ秒ということに留意ください)

```elisp:xsleepサンプル
(progn
  (xclear)
  (dotimes (i 10)
    (xprint i)
    (xsleep 2000)
    )
  )
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140610/b9075f71-8af0-4eb7-c0d8-9f1b0749e722.png)

## xmessage

```
;; ミニバッファにいきなり「i=49」とだけ表示される
(dotimes (i 50)
  (message "i=%d" i))


;; xmessageマクロも第一引数に整数(スリープするミリ秒)を指定しないと、やはりミニバッファにいきなり「i=49」とだけ表示される
(dotimes (i 50)
  (xmessage "i=%d" i))

;; 「i=0」から「i=49」まで、200ミリ秒(0.2秒)ずつ再表示およびスリープしながらカウントアップが見れる(第一引数に整数定数を指定)
(dotimes (i 50)
  (xmessage 200 "i=%d" i))
```

### xsleep を開発・デバッグに使うときのシナリオ

```elisp:my-sleep-test.el
(defcustom *my-sleep* nil "スリープする時間")

(defun my-test ()
  (interactive)
  (dotimes (i 50)
    (xmessage *my-sleep* "i=%d" i)))
```

`*my-sleep*` という変数を定義することでデバッグしたい時だけディレイをかけることができます：

* defcustom でスリープを制御する変数 `*my-sleep*` を nil として定義(第三引数のコメントも必須)
* xmessage の第一引数に `*my-sleep*` を指定しておく
* M-x my-test でマクロを実行 ⇒ ミニバッファにいきなり「i=49」とだけ表示される
* M-x set-variable を実行し、「`*my-sleep*`」 と入力後「200」を入力。
* 再度 M-x my-test でマクロを実行 ⇒ ミニバッファのメッセージがディレイ付きでカウントアップするのが確認できる
* デバッグが終わったら M-x set-variable を実行し、「`*my-sleep*`」 と入力後「nil」を入力。⇒ディレイがなくなる

----

何かお気づきの点がございましたら、コメント等で教えていただけると助かります。