2024-02-23 ,

org-link-completion.el

先日からorg-elisp-link.elに書いていた補完用のコードのうち、elispリンクに限らない一般的な枠組みの部分をorg-link-completion.elへ移動しました。

ブログリンクなどelispリンクと関係ないものを書いているのに org-elisp-link- で始まる関数やらマクロやらを使わないといけないのが何だか気持ち悪く感じてきたので。

基本的な考え方はorg-elisp-link.elにあった時とほとんど同じです。

より強くorg-modeのリンク部分をサポートするという意味で、リンクタイプ部分の補完機能も入れてあります。つまりorg-modeでの入力補完に書いた(pcomplete/org-mode/link関数を修正した)ような [[ の直後部分でリンクタイプを補完することもできるようになっています。ポイントの周辺を解析する関数もそれに合わせてリンクタイプ部分にいることを理解できるように修正しました。解析結果のリストの構造も少しだけ変わっています。

そのリストにアクセスするために以前はnthを連発していたのですが、それを改善するマクロも追加しました。

例えば以前次のようなコードを書きました。

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (when-let ((pos (or org-elisp-link-capf-pos (org-elisp-link-capf-path-parse))))
    (let* ((type-beg (nth 0 pos))
           (type-end (nth 1 pos))
           (path-beg (nth 2 pos))
           (path-end (nth 3 pos))
           (type (buffer-substring-no-properties type-beg type-end)))
      (when (string= type "blog")
        (list
         path-beg path-end
         (cl-loop for file in (directory-files my-blog-dir)
                  when (string-match "\\`\\(.+\\)\\.org\\'" file)
                  collect (match-string 1 file))
         :company-kind (lambda (_) 'file))))))

これを次のようにアクセッサで書けるようにし……

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (when-let ((pos (or org-link-completion-pos (org-link-completion-parse-at-point))))
    (let* ((path-beg (org-link-completion-pos-ref pos path-beg))
           (path-end (org-link-completion-pos-ref pos path-end))
           (type (org-link-completion-pos-ref pos type)))
      (when (string= type "blog")
        (list
         path-beg path-end
         (cl-loop for file in (directory-files my-blog-dir)
                  when (string-match "\\`\\(.+\\)\\.org\\'" file)
                  collect (match-string 1 file))
         :company-kind (lambda (_) 'file))))))

最終的に次のようになりました。

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (org-link-completion-parse-let :path (type path-beg path-end)
    (when (string= type "blog")
      (list
       path-beg path-end
       (cl-loop for file in (directory-files my-blog-dir)
                when (string-match "\\`\\(.+\\)\\.org\\'" file)
                collect (match-string 1 file))
       :company-kind (lambda (_) 'file)))))

最初は分割束縛(この用語もどう日本語にするか悩ましいですが、今回はとりあえずこう書きます)で書こうと思ってpcasecl-destructuring-bindseq-letの三つを理解しやすさ、コード量、意味論、速度、letの入れ子の数など多角的に検討したのですが、実際に使って書いてみたときの全体的なコードが過剰に複雑に見える気がしてnth羅列の方がまだマシという気がしたので最終的に上のようなマクロに落ち着きました。

我々はコード中のマジックナンバーに不快な匂いを感じるよう訓練されているわけですが、それが分割束縛になったところで明示的なインデックス番号が非明示的な語順に変わっただけだということで汚いことには変わりがないんですよね。汚いものは汚いように見えるべきじゃないかと私はよく思います。上のようなマクロを作ってそれだけを使うようにしておけば汚い部分は大分局所化されますね。

で、三つの分割束縛の方法について。

pcaseは総じて優秀だなという印象。少ないコード量で複雑なマッチングと束縛をこなせます。マクロ展開の速度?がこれだけやけに速いみたいなのですが何だろう。チェックが過剰になりがちなのでバイトコンパイルすると最終的な速度はcl-destructuring-bindに追い越されますが。一番の問題点はコードが記号だらけで見づらくなるところですね。それと展開されたコードがかなりの数のletの入れ子になっていました。これ再帰で使ったらすぐにmax-lisp-eval-depthmax-specpdl-sizeの制限に引っかかるのでは? まぁバイトコンパイルすれば緩和されますが。値がnil、4要素のリスト、6要素のリストのいずれかという状況に対してパターンの指定は意味的にやや過剰という面があるでしょう。ちなみにpcase-letというのもありますが、パターンに一致しなかったときの動作は未定義と書かれています。まぁ、最初に全体をwhen-letで受けてから長さをチェックしてpcase-letを使えば良いのでしょうが、大人しくpcaseを使いました。

cl-destructuring-bindの引数リストってあのcl-defunと同じなんですね。ビックリしました。 (&whole pos &optional type-beg type-end path-beg path-end desc-beg (desc-end nil desc-p)) みたいに書いて遊んでました。やべぇなこれ(笑) 面白いから使いたかったのですが、結局どれも使わなかったのと、記述が長くなりがちなのもマイナスでしょうか。というかあまり遊んでると目的外使用感が出てきてしまいますね。あ、plistをこれで受けるなんて使い方もできるのか! 展開されたコードは一番美しかったです。順番にpopしていくだけですね。

seq-letはコードの見た目がシンプルでとても気持ちが良いです。でもシーケンスの長さが一致しないときの挙動が文書化されていません。docstringにもEmacs Lispのマニュアルにも書かれていません。中身はpcase-letを使っているみたいなのですが……。いや、最初の例で4要素のvectorを2つの変数で受けているから大丈夫ということなのかな? もし足りないのはnilになり過剰なのは単に捨てられることが保障されているなら積極的に使っていきたいです。リスト用ではなくシーケンス用なので速度は若干遅いです。まぁ、ほとんどの場合気にする差ではありません。基本的にseq--elt-safeの羅列が生成されるようです。

まぁ、細けぇことはいいんだよ、動けばいいんだ! ということで。